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.
Angular-like Services
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.
A Use-Case
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.
Creating a Service
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.
Providing the service with a Context
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?
Accessing the Service in our Component (The not so right way)
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:
Intermission Test Run
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.
Accessing the Service in our Component (The right(er) way)
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.
DRY - The Contextualizer
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:
Cleaning up the mess
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.
Summary
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.
Top comments (21)
I am trying to wrap my head around this idea and understand if/why this is a great idea. I follow you as far as using a hook for the fetch logic. I always prefer moving the logic for something like this into a custom hook. However, what is the added benefit of using context when you can access the service from the hook? To cache data??? 🤔
Thank you for the comment! :)
The way I see it, the benefit comes when you're testing. When you need to test a component that relies on the service, you include a context provider when you render for the test. You can then set the value for that provider to be a mock implementation of that service.
Does that make sense?
Ahh, yeah! That makes sense! I still have a lot to learn when it comes to testing, so I really wasn't thinking about that.
Glad that clears it up! :)
I know Jest comes with a set of functions to be able to mock components, and I'm not veery used to using them. I find this particular solution makes it easier to mock components on the fly, while also enabling abstracting away concerns. It's entirely possible that there are more elegant built-in ways that I just haven't explored yet :)
If you think it might be useful, I can see about making a tutorial for how to think when writing tests in this manner!
Well, I do not see your solution as a bad implementation at all, I just feel you sometimes tend to get a bit of "context-provider-hell" working with things like styled components and Apollo-client etc. I just wich this could be done in a cleaner way in React.
I'll admit that I just very shortly touched into StyledComponents... I did not like it at all :P.
Jack seems to have built a possibly viable solution for a full-fledged DI-system. It's probably more refined than mine!
LoL 😂 Styled components is a bit of a love/hate relationship I guess. But it can be really powerful when you want to do things with styling that are a bit out of the ordinary. I used it yestoday to create a cool "unwrap" effect animating some nested nav-menus where I the height of elements that would change depending on what is placed in the menu: Stuff you just can't do with "traditional" ways of styling. I bookmarked Jacks Solution - looks like something that might come in handy!
I felt the exact same way when i moved from angular to react a few years ago, which is why I wrote and constantly plug react-jpex for DI:
dev.to/jackmellis/dependency-injec...
Great article!. What about tree-shakability ? I assume Angular services are tree-shakable despite being singleton.
IIUC, If we registered all our services in the
GlobalServices
, then it would make it available across the app but we loss the benefit of tree-shakability, right ? meaning these services are there before they are needed (i wonder how that would affect initial page load on a very big react app with lot of services registered). I understand we could choose not to provide them in the GlobalServices rather provide only where it's needed. then, in that case due to the nature of context. we will end up having to provide my services in multiple places which is bit of a extra step and cumbersome IMO. Or Did I misunderstood something ?I like your approach with the DI and the generic way to get new services.
DI is something that is missing from React apps
Thanks a bunch! Glad it was helpful. And I agree. Some kind of DI-container would have been really helpful straight from the box
Good idea, but is it possible to make hell provider if have some much context to wrap
Hi! Thanks for your comment. I'm afraid I'm not sure I understand completely what you mean? :)
yeah i mean, if so much context have been made, it could make hell provider, can you tell, how to void ?
If I get you correctly, you're worried that you'll clog the template with services? My solution to this was to put all Services inside a GlobalServices that is then wrapped around the App. Does that answer your question?
yeah it's good solution, but there's any approach method to make Dependecy Injection without using context?
I'd assume so! If you check my comments, you can see that Jack has made a solution that seems very viable!
I've been learning TypeScript for a few weeks now and still figuring out how to make things in the best way using it. This post was a really great thing to see. Thanks 👍
Oh hey! Very glad that you found it helpful :) Thanks a bunch