The definition of Dependency Injection in software programming is:
Dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally.
// Direct Coupling of dependency and MyApplication APP public class MyApplication { // coupling private EmailService email = new EmailService(); public void processMessages(String msg, String rec){} } // Simple way of implementing DI public class MyApplication { private EmailService email = null; // Now the Dependency is Injected in the constructor public MyApplication(EmailService svc){ this.email=svc; } public void processMessages(String msg, String rec){} }
Dependency as any static imports
we do in that file.// This is an example of Coupled Dependecny import getData from "..";
inject them via Props
in React, and we have been using this for a long time to pass in data to our React components.// import getData from ".."; Delete this const MySuperCOmponent = ({getData, ...}) => {}
This works but it has some drawbacks:
So what could be a better solution, its Dependecny Injection via Context
In React we are already using Context to share state, in a un coupled way with components.
We can easily extend this basic concept for dependencies.
Let's see how we will go about it
const AppDIContext = createContext();
import { getDefaultListData } from "../hooks/getListData"; import { useLogger } from "../hooks/useLogger"; const DEFAULT_DI_CONTAINER = { registry: { getListData: getDefaultListData, useLogger, }, resolve() { return this.registry; }, };
container
here is the DI container will all the values that we want to Inject.const AppDIProvider = ({ children, container }) => { return ( <AppDIContext.Provider value={container}> {children} </AppDIContext.Provider> ); };
const useDIContainer = () => { const container = useContext(AppDIContext); if (!container) { throw new Error( "DI container not found. This hook can only be used in a branch of the DI container" ); } return container.resolve(); };
Since the return value of this hook is an Object with all the Dependecy's as key value pair, we can destruct it and consume what we need.
import { useDIContainer } from "../di-container/AppDependencyContext"; const Lists = () => { const { getListData, useLogger } = useDIContainer(); useLogger(); const data = getListData(); return (); };
// ViewA <AppDIProvider container={DEFAULT_DI_CONTAINER}> <h2>This is a default Container Example</h2> <Lists /> </AppDIProvider>
This is very usefull when we want to extend from an exiting code base or want it to behave differently at runtime, only thing we need to do is update the container value.
Let's suppose we want to build a UI similar to our ViewA app but with slight differences in our new app ViewB.
Only thing we need to do is create a new registry container that overrides the dependecny we need and extend the ones already there.
import { getCustomListData } from "../hooks/getListData"; import { DIType } from "from ViewA"; import { DEFAULT_DI_CONTAINER } from "from ViewA"; const CUSTOM_DI_CONTAINER = { registry: { // this makes sure we get all the default functionality and override or add new ones ...DEFAULT_DI_CONTAINER.registry, getListData: getCustomListData, }, resolve() { return this.registry; }, };
import { AppDIProvider } from "from ViewA"; import Lists from "from ViewA"; import { CUSTOM_DI_CONTAINER } from "./di-container/custom-container"; const ViewB = () => { return ( <AppDIProvider container={CUSTOM_DI_CONTAINER}> <h2>This is a Custom Container Example</h2> <Lists /> </AppDIProvider> ); }; export default ViewB;
This repo is boot strapped with Vite, running the following commands to get going:
npm install npm run dev
The repo has following:
app.tsx
that just mimics a remote housing two host ViewA
and ViewB
.