In my previous posts, I have written quite a few times about SharePoint Framework service scopes (I will add links at the end of the article). In short, Service Scopes are the SPFx implementation of the Service Locator pattern i.e. a single shared “dictionary” where services (either oob SPFx or custom) are registered and can be consumed from any component in the application.
For example, without using service scopes, if we wanted to make a call to the Microsoft Graph (using MSGraphClient) from within a deeply nested react component, either we would have to pass in the SPFx context down all the components in the tree, or maybe a create a custom service which returns the web part context, and then call that service from within our nested component. Or maybe use redux to globally maintain the context in a single state object.
But with all these approaches (there may be more), testing the components would be difficult as they would have a dependency on the SPFx context which is hard to mock. Waldek Mastykarz has a great post on this.
Also, from a maintenance point of view, it could get tricky as almost all our components would start to depend on the entire context and we could easily loose track of which specific service from the context is needed by the component.
Now with my previous posts on service scopes, even though we were removing the dependency on the SPFx context, one issue still remained that the SPFx service scope was still needed to be passed into the component. We were just replacing the SPFx context with the SPFx service scopes. While this was good from a testing point of view, it wasn’t great for maintainability.
Fortunately, in the recent versions of SPFx, React 16.8+ was supported which means that we can take advantage of React hooks. Specifically, the useContext hook. This gives us a very straightforward way to store the SPFx service scope in the global react context (which is different to the SPFx context) and then consume it from any component in our application no matter how deeply nested it is.
Let’s see how to achieve this. In these code samples, I am using SPFx v1.10 which is the latest version at the time of writing.
1) The Application Context object
First, we need to create the React application context object which will be used to store and consume the service scope. For now I am only storing the serviceScope in the context. Other values can be stored here as well.
import { ServiceScope } from ‘@microsoft/sp-core-library’; | |
import * as React from ‘react’; | |
export interface AppContextProps { | |
serviceScope: ServiceScope; | |
} | |
export const AppContext = React.createContext<AppContextProps>(undefined); |
view rawAppContext.ts hosted with ❤ by GitHub
2) React Higher Order Component (HOC)
React hooks can only be used from functional components and not classes. With the SPFx generator creating classes by default and hooks being fairly new, I am sure there is a lot of code out there already which use classes and not functional components. Changing all code to use functional components instead of classes is a non-starter.
Fortunately, there is a way to use react hooks with classes by creating a Higher Order Component (HOC) which is a functional component. We can wrap all our class components with this HOC and safely consume the useContext hook from within this component.
(Update: If you are interested in going down the “full hooks” approach and doing away entirely with classes, Garry Trinder has got you covered. He has created a fork which only uses functional components and hooks so we don’t need the HOC. If you want to take this approach, check out the code here: https://github.com/garrytrinder/spfx-servicescopes-hooks)
import * as React from ‘react’; | |
import { useContext } from ‘react’; | |
import { AppContext } from ‘./AppContext’; | |
//Infuse all components wrapped with this Higher Order Component with the serviceScope | |
export const withServiceScope = (Component: any) => { | |
return (props: any) => { | |
const appContext = useContext(AppContext); | |
return <Component serviceScope={appContext.serviceScope} {…props} />; | |
}; | |
}; |
view rawwithServiceScope.tsx hosted with ❤ by GitHub
3) SPFx web part
Next, we update our SPFx webpart to only pass in the serviceScope once to our top level component:
import * as React from ‘react’; | |
import * as ReactDom from ‘react-dom’; | |
import { Version } from ‘@microsoft/sp-core-library’; | |
import { BaseClientSideWebPart } from ‘@microsoft/sp-webpart-base’; | |
import HelloWorld from ‘./components/HelloWorld’; | |
import { IHelloWorldProps } from ‘./components/IHelloWorldProps’; | |
export interface IHelloWorldWebPartProps { | |
description: string; | |
} | |
export default class HelloWorldWebPart extends BaseClientSideWebPart <IHelloWorldWebPartProps> { | |
public render(): void { | |
const element: React.ReactElement<IHelloWorldProps> = React.createElement( | |
HelloWorld, | |
{ | |
description: this.properties.description, | |
serviceScope: this.context.serviceScope //Only need to pass in serviceScope once to the top level component | |
} | |
); | |
ReactDom.render(element, this.domElement); | |
} | |
protected onDispose(): void { | |
ReactDom.unmountComponentAtNode(this.domElement); | |
} | |
protected get dataVersion(): Version { | |
return Version.parse(‘1.0’); | |
} | |
} |
view rawHelloWorldWebPart.ts hosted with ❤ by GitHub
4) Top level React component
Our top level component will need to be wrapped with the AppContext so that any nested component will be able to consume it. This just needs to be done once on the top level react component. You will notice that the HelloUser child component does not need any props passed in.
import * as React from ‘react’; | |
import { IHelloWorldProps } from ‘./IHelloWorldProps’; | |
import HelloUser from ‘./HelloUser’; | |
import { AppContext } from ‘../common/AppContext’; | |
export default class HelloWorld extends React.Component<IHelloWorldProps, {}> { | |
public render(): React.ReactElement<IHelloWorldProps> { | |
return ( | |
//Wrap the topmost component with the Context provider. Also initialise the object with the serviceScope passed in from the SPFx webpart. | |
<AppContext.Provider value={{ serviceScope: this.props.serviceScope }}> | |
<div> | |
{/*HelloUser and any other nested components will have the serviceScope property filled. | |
Even if the components are deeply nested*/} | |
<HelloUser /> | |
</div> | |
</AppContext.Provider> | |
); | |
} | |
} |
view rawHelloWorld.tsx hosted with ❤ by GitHub
5) Child component
import * as React from ‘react’; | |
import { ServiceScope } from ‘@microsoft/sp-core-library’; | |
import { withServiceScope } from ‘../common/withServiceScope’; | |
import { MSGraphClientFactory, MSGraphClient } from ‘@microsoft/sp-http’; | |
interface IHelloUserProps { | |
serviceScope: ServiceScope; | |
} | |
interface IHelloUserState { | |
Name: string; | |
} | |
class HelloUser extends React.Component<IHelloUserProps, IHelloUserState> { | |
private msGraphClientFactory: MSGraphClientFactory; | |
constructor(props: IHelloUserProps) { | |
super(props); | |
this.state = { Name: ”}; | |
//this.props has the serviceScope property due to the withServiceScope Higher Order Component passing it in. | |
this.msGraphClientFactory = this.props.serviceScope.consume(MSGraphClientFactory.serviceKey); | |
} | |
public render(): React.ReactElement<{}> { | |
return <div> | |
{this.state.Name && | |
<span>Hello {this.state.Name}</span> | |
} | |
</div>; | |
} | |
public componentDidMount() { | |
this.msGraphClientFactory.getClient() | |
.then((client: MSGraphClient): void => { | |
client | |
.api(‘/me’) | |
.get((error, user: any, rawResponse?: any) => { | |
this.setState({ Name: user.displayName }); | |
}); | |
}); | |
} | |
} | |
//This part is key as it wraps the current component with a Higher Order Component allowing the serviceScope to be passed in as a property. | |
export default withServiceScope(HelloUser); |
view rawHelloUser.tsx hosted with ❤ by GitHub
And that’s it! This way, we can use the React useContext hook to globally share our SPFx service scope.
Hopefully you have found this post helpful! All code is added in the GitHub repo here: https://github.com/vman/spfx-servicescopes-hooks
Also, if you are interested, here are all my previous articles on SPFx service scopes:
Getting the current context (SPHttpClient, PageContext) in a SharePoint Framework Service
Want to learn more? Check out this post: Build a breadcrumb using SPFx extensions
About the Author:
Technical Architect. SharePoint/Office 365/Azure Developer.
Reference:
Deshpande, V. (2020). SPFx: Using React hooks to globally share service scope between components. Available at: https://www.vrdmn.com/2020/02/spfx-using-react-hooks-to-globally.html [Accessed: 11th June 2020].