Dependency Injection in React
Leverage React's built-in dependency injection patterns for more testable, maintainable code
Introduction
Dependency injection (DI) is a design pattern where dependencies are provided to a component rather than created internally. React has built-in support for DI through props and Context, making it easy to write testable, maintainable code.
What is Dependency Injection?
Instead of creating dependencies inside a component, you pass them in:
// ❌ Without DI - hard to test
function UserService() {
const api = new ApiClient(); // Hard-coded dependency
const getUser = async (id) => {
return await api.fetch(`/users/${id}`);
};
}
// ✅ With DI - easy to test
function UserService({ api }) {
const getUser = async (id) => {
return await api.fetch(`/users/${id}`);
};
}
React's Built-In DI: Props
Props are the simplest form of dependency injection in React.
function UserList({ users, onUserClick }) {
return (
<ul>
{users.map((user) => (
<li key={user.id} onClick={() => onUserClick(user)}>
{user.name}
</li>
))}
</ul>
);
}
// Inject dependencies via props
<UserList users={fetchedUsers} onUserClick={handleUserClick} />;
React Context for DI
Context provides a way to share dependencies across the component tree without prop drilling.
// Create context
const ApiContext = createContext(null);
// Provide dependency
function App() {
const api = new ApiClient();
return (
<ApiContext.Provider value={api}>
<UserList />
</ApiContext.Provider>
);
}
// Consume dependency
function UserItem({ userId }) {
const api = useContext(ApiContext);
const [user, setUser] = useState(null);
useEffect(() => {
api.getUser(userId).then(setUser);
}, [userId, api]);
return <div>{user?.name}</div>;
}
Custom Hooks for DI
Custom hooks can encapsulate dependency injection logic.
function useApi() {
return useContext(ApiContext);
}
function useUser(userId) {
const api = useApi();
const [user, setUser] = useState(null);
useEffect(() => {
api.getUser(userId).then(setUser);
}, [userId, api]);
return user;
}
// Clean component with injected dependency
function UserProfile({ userId }) {
const user = useUser(userId);
return <div>{user?.name}</div>;
}
Testing with DI
Dependency injection makes testing easier by allowing you to mock dependencies.
// Mock API for testing
const mockApi = {
getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }),
};
// Test with injected mock
render(
<ApiContext.Provider value={mockApi}>
<UserProfile userId={1} />
</ApiContext.Provider>,
);
expect(mockApi.getUser).toHaveBeenCalledWith(1);
Best Practices
Inject at the Top Level
// ✅ Good - inject at app root
function App() {
const api = createApi();
const auth = createAuth();
return (
<ApiContext.Provider value={api}>
<AuthContext.Provider value={auth}>
<MainApp />
</AuthContext.Provider>
</ApiContext.Provider>
);
}
// ❌ Bad - inject deep in tree
function ChildComponent() {
const api = createApi(); // Creates new instance every render
}
Use Multiple Contexts
Separate concerns with multiple context providers.
<ApiContext.Provider value={api}>
<AuthContext.Provider value={auth}>
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
</AuthContext.Provider>
</ApiContext.Provider>
Document Dependencies
/**
* UserProfile Component
* @param {Object} props
* @param {number} props.userId - User ID to display
* @param {Object} props.api - API client (injected via context)
*/
function UserProfile({ userId }) {
const api = useContext(ApiContext);
// ...
}
Conclusion
React's props and Context provide powerful dependency injection capabilities. Use them to write more testable, maintainable components.
#react #dependency-injection #architecture