Creating Reusable React Components for SharePoint Framework Solutions

Lessons Learned from the #SPShire Project

This is the one of my lessons learned from the Shire Hub Intranet project, based on the forthcoming SharePoint Communication Sites.

The lessons in this blog series are:

  1. Previewing and Opening Office Documents from the SharePoint Framework
  2. Using the OneDrive File Picker in SharePoint Framework Solutions
  3. Creating Reusable React Components for SharePoint Framework Solutions
    (this article)

This project on github contains the sample solution for all three articles.

Creating Reusable React Components for SharePoint Framework Solutions

What with displaying iFrames and managing their messages, as well as a validated link entry form, there a fair chunk of code involved in the link picker. Shire has about half a dozen web parts that use it in one way or another, and we wanted to minimize the code duplication.

Every SPFx web part project comes with a React component that renders the web part; in this case it’s the LinkPickerSample.tsx component. However if you browse the sample code, you’ll notice there’s a 2nd component, outside of the web parts directory under src/components. The component is in the LinkPickerPanel folder, ready to be shared by any number of web parts that might need the ability to choose a file in SharePoint.

If you want to use the LinkPickerPanel in your project, everything you need is in the LinkPickerPanel folder, including a readme that explains how to use the component.

Like every React component, the LinkPickerPanel has properties and (optional) state. When a React component is created, it is passed a set of properties which don’t change until the component is re-rendered.


export enum LinkType {
 doc = 1 << 0,
 page = 1 << 1,
 image = 1 << 2,
 folder = 1 << 3,
 any = doc | page | image | folder
}

export interface ILinkPickerPanelProps {
 className?: string;
 webAbsUrl: string;
 linkType: LinkType;
}

The component also stores state, which survives refreshes. Here is the LinkPickerPanel’s state.


// The left navigation selects what kind of link picking to do
export enum NavState { site, link }

export interface ILinkPickerPanelState {
isOpen? : boolean;    // true if the panel is open
navState?: NavState;  // the navigation selection
isUrlValid?: boolean; // true if the URL is valid
url?: string;         // the link
}

When a component changes its state, it and its children are re-rendered, possibly with new property values, and React very efficiently updates the user interface in the browser. You may find it convenient to use optional properties, in the state interface so you can update only the members you need in setState().

Hopefully the state values are pretty obvious. isOpen tracks whether the panel itself is open, and this one might be a little controversial (more on that later). navState is an enumeration of the left navigation tabs, which allow browsing with the OneDrive file picker or entering a URL manually. navState determines whether the “Site” or “Link”  tab is currently active. isUrlValid remembers if the link was validated properly, and url is the actual link that was chosen.

To create your own React component  in TypeScript, begin by creating a class that extends React.Component with properties and state specified as type arguments. This makes both type safe. Then make sure you include a render function which returns the React child components you wish to render.


export default class LinkPickerPanel
extends React.Component<ILinkPickerPanelProps,
ILinkPickerPanelState> {

public render(): JSX.Element {

return (
<Panel isOpen={this.state.isOpen}
onDismissed={this.removeMessageListener.bind(this)}
className={styles.linkPicker}
hasCloseButton={false}
type={ PanelType.extraLarge }
isLightDismiss={true}
onDismiss={this.onCancelButtonClick.bind(this)}>
<div>{/* other components ... */}</div>
</Panel>);
}

isOpen and the Principle of Lifting Up State

React has a concept of  “lifting up state” – that is, when two or more components share some state, the parent component should maintain the state and pass it into the child using a property. In this case the “state” is the open/closed state of the panel.

The Office UI Fabric components follow this practice. The Panel has an “isOpen” property, and the Dialog has a “hidden” property – not exactly the ultimate in consistent naming but they both follow the pattern of making the parent component manage the Panel or Dialog’s open/closed state, and passing it in a property. In the render() function, the parent queries its own state in setting the property on its child. The parent then sets its own state to open when it wants the panel to open,  and to closed in the onDismiss() callback in order to close the panel.

The thing is that a web part is not exactly a React component, and having it maintain this kind of state did not seem straight forward. Our initial implementation used the main React component as a helper of sorts, but this seemed convoluted. In the end, I was unable to resist the utter simplicity of providing a method on the LinkPickerPanel that returns a Promise for the URL. This shaved off dozens of lines of code, and removed the dependency on the main component. I think it’s much more readable and intuitive, but then, I’m new to React and may be committing a major faux pas! I’d welcome your suggestions in the comments please.

In order to call the method, the web part needs a reference to the LinkPickerPanel component. The “safe” way to do that is to use a function ref. This isn’t so unusual but it may be tough to figure out how to do it so it’s type safe and without the benefit of JSX, so here’s the code. This is from the web part’s render() function.

public render(): void {
  const element: React.DOMElement<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> =
    React.DOM.div({
      children: [
        React.createElement(
          LinkPickerSample,
          {
            webAbsUrl: this.context.pageContext.web.absoluteUrl,
            url: this.properties.url,
          }),
        React.createElement(
          LinkPickerPanel,
          {
            webAbsUrl: this.context.pageContext.web.absoluteUrl,
            linkType: LinkType.any,
            ref: (ref) => { this.linkPickerPanel = ref; }
          })
      ] });

  ReactDom.render(element, this.domElement);
}

As you can see, I’m rendering not one but three components here. The first is a <div>, which exists purely to hold the other two components (a single root is mandatory in React!). The second is the LinkPickerSample component that yeoman created; it’s the main UI for the web part. The third is the LinkPickerPanel, initially closed but ready to open on request.

NOTE: This Babel test page is great for translating JSX into createElement() calls, as required in an SPFx web part.

When a user clicks the button to open the link picker panel, the web part calls the method on this.linkPickerPanel, where it has stored the reference to the link picker panel component. A Promise makes it easy to update the web part based on the chosen value. If the user closes the panel without choosing a valid link, the promise is rejected, but there’s no need to check that here because nothing is exactly what we need to do in that case.

private getLink() {
  if (this.linkPickerPanel) {
    this.linkPickerPanel.pickLink()
    .then ((url) => {
      this.properties.url = url;
      this.context.propertyPane.refresh();
    });
  }
}

Style Sheets

It’s not enough to separate the code for the component to be reusable; other pieces such as style sheets and resource strings need to be separated as well. In the case of style sheets, it was easy! Just create an .scss file wherever you want in your structure, and the gulp task will build the typescript bindings to the style names. (If you’re not familar with style modules, see this article and scroll down to “CSS Modules”).

So at the top of my component I pull in its local styles, and WebPack faithfully injects them into the bundle and puts them on the page.

import styles from './LinkPickerPanel.module.scss';

// ... later on, within the class, reference the styles

return(
  <Panel isOpen={this.state.isOpen}
    onDismissed={this.removeMessageListener.bind(this)}
    className={styles.linkPicker}
    hasCloseButton={false}
    type={ PanelType.extraLarge }
    isLightDismiss={true}
    onDismiss={this.onCancelButtonClick.bind(this)}>
    {*/ children go here */}
  </Panel>
);

MultiLingual Support

As easy as it was to separate out the CSS styles for my shared component, the language strings remain a mystery. SPFx supplies localization files for each web part, which is just awesome since it’s right in the template and encourages using resource strings instead of string literals from the get-go. SPFx generates a resource file for each language that is deployed along with the WebPack bundle, and it automatically pulls in the right resource file depending on the end-user’s language. This is an excellent design because it means users only download the language strings they need, and developers don’t need to deal with the language selection logic.

But I don’t want my strings in any web part! I want them in their own place, tied to my reusable component, and I was unable to figure out a way to get SPFx to add them to the resource files the way it does with web parts. (Comments please if you’re in the know!)

As a work-around, I still put user-visible strings in a language file, but it’s just bound to the component. At the moment I don’t have a way to switch languages, but I’m sure that wouldn’t be too difficult if I needed it; mainly for now I wanted to just maintain the use of a resource file so the code wouldn’t get littered with string literals.

Conclusion

As you can tell, I’m having a blast working on this project with so many brilliant minds, working on leading edge technology. We’re all learning every day, and will post more over time. Thanks for reading!

 

4 thoughts on “Creating Reusable React Components for SharePoint Framework Solutions

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s