Dependency Inversion in React
Using React Context
Dependency inversion is satisfied by using interfaces in dependencies.
interface CoffeeMachine {
expresso(): void;
}
function CoffeeShop(coffeeMachine: CoffeeMachine) {
return {
expresso: () => {
coffeeMachine.expresso();
}
}
}
In this example we simply create a CoffeeShop that can make an espresso, the coffee machine implementation can be changed with another implementation and the CoffeeShop would not know the difference.
When I started developing in React I’ve found a lot of example online using concrete implementation directly inside a component.
import axios from "axios";
import * as React from "React";function PostComponent() {
const [post, setPost] = React.useState(null);
axios.get(`url`).then(res => {
setPost(res.data);
})
return post && <div>
{post.title}
</div>
}
This component is depending on axios. How do we refactor this code to follow the dependency inversion principle?
What if we just wrap axios in a http function?
import axios from "axios";function Http() {
return {
get: async<T>(url: string): Promise<T> => {
return axios.get(url).then(data => data.data);
}
}
}
And now the component will import http.
import { http } from './http';function PostComponent() {http().get("url").then(...
...
}
As you can see PostComponent is still depending indirectly on axios. So you don’t really depend on an abstraction.
What we really want is to make sure PostComponent does not depend on axios directly (see the image below).
- Post Component depends on IHttp
- Http implements IHttp and depends on axios
- Post Component does not depend on either axios or http
We could create a wrapper of axios and pass it in the parameters of the component as interface
Yes. Let’s do it.
1 — Create the interface IHttp
http.interface.ts
interface IHttp {
get<T>(url: string): Promise<T>;
}
2 — Http Implement the interface
http.ts
import axios from "axios";function Http(): IHttp {
return {
get: async<T>(url: string): Promise<T> => {
return axios.get(url).then(data => data.data);
}
}
}
3 — Pass IHttp to the component
import { APP_CONTEXT } from './context';
import { Http } from './http';
import { IHttp } from './http.interface';
import { appDependencies } from './dependencies';
const http: IHttp = Http();export function App(): JSX.Element {
return (
<PostComponent http={http}/>
);
}
As you can see PostComponent does not depend on axios! Yuppieeee
import { IHttp } from './http.interface';function PostComponent({http: IHttp}) {
...
}
This is perfect when your application is really small.
What if there are another 4–5 components between App and PostComponent? Do we move the dependency to the closest component? Why should the closest component know about http?
Ideally we want to create the http implementation at the app level because in the future it will require more configuration and we want to create it once. We don’t want to spread this implementation around the app.
So, what we do?
We could use React.Context!
In react we have the Context API that can be used as a Service Locator
1 — Create your context as null and define the interface
context.ts
import { IHttp } from './http.interface';export const APP_CONTEXT: React.Context<AppContext> = React.createContext(null);
export interface AppContext {
http: IHttp;
}
2 — Create your dependencies initialisation
dependencies.ts
import { IHttp } from './http.interface';
import { Http } from './http';const http: IHttp = Http();export const appDependencies: AppContext = {
http: http
}
3 — Add the context to your app
import * as React from "React";
import { APP_CONTEXT } from './context';
import { appDependencies } from './dependencies'export function App(): JSX.Element {
return (
<APP_CONTEXT.Provider value={appDependencies}>
<PostComponent/>
</APP_CONTEXT.Provider>
);
}
4 — Use it in PostComponent
import * as React from "React";
import { APP_CONTEXT } from './context';function PostComponent() {
const http: IHttp = React.useContext(APP_CONTEXT).http;
const [post, setPost] = React.useState(null);
http.get(`url`).then(res => {
setPost(res.data);
})
return post && <div>
{post.title}
</div>
}
Done.
- PostComponent depends on AppContext, so on IHttp
- AppDependencies implements the AppContext and depends on Http, so on Axios
What is the benefit of creating and developing your application with this approach? React context allows you to take advantage of dependency inversion. You can provide a different implementation of the same interface
export function App() {
return (
<POST_CONTEXT.Provider value={dependenciesA}>
<PostComponent/>
</POST_CONTEXT.Provider/>
<POST_CONTEXT.Provider value={dependenciesB}>
<PostComponent/>
</POST_CONTEXT.Provider/>
<POST_CONTEXT.Provider value={dependenciesC}>
<PostComponent/>
</POST_CONTEXT.Provider/>
);
}
Conclusions
Benefits
- This solution will respect dependency inversion principle
- Unit test will be easier
- Components are not coupled to core libraries
- Switching dependencies is efficient
Disadvantages
- The creation of dependencies has to be done manually, you don’t have IOC (Inversion of Control)
Thanks to Giulio that helped me understand the differences between IOC, DI (Dependency Injection) and DIP (Dependency Inversion Principle)