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.
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)
3) SPFx web part
Next, we update our SPFx webpart to only pass in the serviceScope once to our top level component:
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.
5) Child component
Due to the Higher Order component and the useContext hook, we are able to access the serviceScope property from withing the child component. We can grab the MSGraphClient from the serviceScope and start making calls to the Graph:
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:
SharePoint Framework: Org Chart web part using Office UI Fabric, React, OData batching and Service scopes
Getting the current context (SPHttpClient, PageContext) in a SharePoint Framework Service
Service Locator pattern in SPFx: Using Service Scopes
Service Locator pattern in SPFx: Using nested scopes to work with multiple components
5 comments:
Fantastic article
Hi Vard, I hope you are doing well.
Maybe I am losing something but I don't see the need of React hooks or HOC to achieve the same result.
React Context existed much before hooks (https://reactjs.org/docs/context.html), and you can use that API both in the Provider (as you are doing) as in the Consumer. In the child component you just need to import AppContext.ts and use static contextType = AppContext;
Hi Rafa, I am good thanks, Hope you are doing great as well!
Yo are right, in simple scenarios you could get away with just doing static contextType = AppContext but as mentioned in the article you posted, it could get tricky if the child component was dependent on multiple contexts. In that scenario, you would have to resort to something like this: https://reactjs.org/docs/context.html#consuming-multiple-contexts
(An argument can definitely be made that why not combine both contexts in a single context but that would make component re-use slightly more complex)
Also, I am not a big fan of wrapping the child component in a tag every-time. Using HOC with hooks or even using only hooks makes the code simpler in my opinion.
I am not a fan of extra wrapping either, and technically the HOC is wrapping the child component as well.
With React Context for classes you have two different options:
- Wrapping your child component with the AppContext.Consumer component that uses render props.
- Use the mentioned Class.contextType (static contextType = AppContext). In this case, the context is available in this.context and no wrapping is needed.
I think the main benefit you get with HOC is that you are able to specify which properties from the context get passed into the child component (e.g. the service scope in this case). With doing using 'static contextType = AppContext' you are passing the entire context again. I think both options are valid depending on the use case.
Post a Comment