Having worked in an Angular-development team for several years, it was exciting for me to learn React and it's more lightweight approach to web-development.
I quickly took to most of the ways that React 'does things', but after three years of working with Angulars very loose coupled development, it was hard to wrap my mind around the fact that React did not provide a proper Dependency Injection functionality out of the box.
Here I detail a technique to get a certain degree of Dependency Injection with React by providing services through the Context API and making them accessible through hooks. It's a very simple solution, so much so that I'm wondering if it's either blatantly obvious or not a very good design. I am presenting this as both a source of inspiration to new React-developers (of which I am a part of) and as a case study for critique and feedback for the more experienced React-developers out there.
In apps with a certain size and complexity, it's handy to abstract away certain functionality and isolate concerns away into individual and independent parts. These parts - called services - can serve as a single point of entry for a particular responsibility in your app, such as accessing a particular API, storing data in local storage or maintain some form of state (a few examples in a sea of possibilities).
A service should have limited - if any - knowledge of the world outside itself. It should have only a few methods. This makes it easy to test both the service and the components that use the service in isolation, possibly reducing the need for integration testing in favor of more surgical unit tests.
Let's envision an absurdly simple page. This page should display a list of all employees through an Employee-component. The employee data is received from an external API.
The data-object for the employee looks like this (note that I'm using Typescript, so for all you purists out there, feel free to look away from any and all strong typing)
Our component looks like the following:
Take a while to take in the stunning and complex code that is our employee-display component. As is the usual case for React apps, our component takes in the employee-objects as props. From what I understand, it was most usual to let data-objects such as these travel down through the component-trees from a higher level component. After the Context API, it has become easier to access these values without relying on multitudes of prop-passing. But we're gonna take it a step further than that.
Let's first create a service. The sole responsibility of this service should be to - when prompted - send an api-call to an external webpage and when the call was resolved, return the values it received. If you use Typescript, you may want to start by defining an interface with the required functionality:
Not very fancy, but it serves our purposes excellently. A parameterless method that returns a Promise with our list of employees (which will be fulfilled once we receive a response).
Note that I realize that the use of I to denote an interface is a somewhat controversial topic in the Typescript-world. I like it better than adding Interface as a post-fix to the interface, and it's better than coming up with fancier name for the component. Always follow the local guidelines for naming conventions, kids!
Let's now create a functional component. This component will have the implementation for the interface:
Not very impressive. But it too will do. As you can see, my linter is complaining about missing usage. We'll fix that in a moment.
Let's now make the service available through our app with the help of the Context API. We'll create a context outside the component, and we'll provide the implementation we just made through it:
To make things a bit easier for myself in this test case, I extended the component as an FC, which allows me to access the components children out of the box. You may not want to do this
At any rate. We now have a Context that contains our implementation of the EmployeeService. Due to how the Context API system works, only the children of this component will be able to access it. But how should we access it?
Let's make a parent component for our EmployeeComponent. (Let's call it EmployeePage.tsx) The responsibility of this component is to access our service, get the data and pass it onto our EmployeeComponent as a prop:
(A little oops here. Make sure that the useEffect-hook takes in employeeService as a dependency)
Without going into all the specifics, we import the context, and with the useContext-method we extract the service. In the useEffect-hook we make the call, and when the results are returned, we pass them on as a prop to the Employees-component.
We then wrap the App-component in index.tsx with out Service:
Our service is now accessible within the entire App by importing and using the Context.
Looking good? Well, not quite. First of all. We can't be sure that we won't make a mistake and try to call the useContext-hook in a component that isn't a child of the Provider. Secondly, we could make the use of the service more apparent in our code.
Let's make a few changes. In the EmployeeService, we'll stop exporting the EmployeeServiceContext. Instead we will create a hook that uses the Context for us. While we're at it, let's be sure that a value is provided, and throw a helpful error message if it isn't:
Now let's refactor our EmployeePage.tsx code to reflect these changes. It feels so much more apropriate to let the hook handle the possibility of undefined values:
Okay. Let's see how this works in action. Add some dummy data to the EmployeeService-class and make sure the EmployeePage is a child of AppComponent and do a test run:
Hey, great. It works! We shouldn't be quite satisfied just yet though.
For this special case, our code is perfectly fine. But since we are setting this system up anticipating at least a good number of services, this will get cumbersome fast. Checking that all contexts for each hook exists, and also writing a test for each service? Ugh. This is an excellent case of DRY in action. Let's not do that.
Let's create a central hub for all our services. This hub will keep track of all our contexts and - when a particular service is asked for - it will check if it exists and return an apropriate error if it doesn't.
We'll make two files. The Contextualizer.ts and the ProvidedServices.ts. The latter is a simple enum that will contain all the services that exist within our app. This will be handy for us, and might also be handy for the onboarding process of future developers. Let's make this one first:
(It's probably fine to include this with the Contextualizer. I left it as its own file so its easier to use as a kind of service-encyclopedia)
Then it's time to set up our Contextualizer:
With this class, we generalize the creation of new services and retrieving them. Note that we still want to provide custom hooks for each service, for the sake of following React guidelines.
Here we also take into account the cases of a service not having been created at all, as well as if the service is not available from the component it is called it.
(You may get a lint-warning here that you should never use the
useContext-method outside of a component. I chose to ignore this warning, as it will ultimately only be called inside an component anyway. )
(Finally, you should be able to remove the useEffect-dependency. It's possible you actually have to for anything to appear.)
We have succesfully generalized our system for creating contexts and retrieving their value through hooks. In our EmployeeService-class we can now reduce the previously rather obtuse Context-related code to the following two lines:
We're almost done. We can now create services, and provide them in our app with a few lines of code (and an entry to our enum). But there's one little detail that remains:
Our index.tsx will easily get clogged if we're gonna be putting all our services in there. Let's instead create a little component solely for containing and keeping all our services. Let's call it GlobalServices.tsx, and lets replace the currently existing EmployeeService in index.tsx with it:
As we create more services to our application, we can add them in this list. Just remember that if you have services that rely on other services, they must be placed as a child of that service.
This is a very bare-bones example of how a pattern for allowing services in React can be done. I'm sure it's not perfect. Feel free to suggest improvements, critique it or give it tons of compliments in the comments section. If there are any questions, feel free to bring them forth too.
If people are positive and find this helpful, I might write a little explanation for how this pattern makes unit-testing services and components easier at a later time.