Dependency Injection (DI) is a design pattern that promotes loose coupling and easier testing by externalizing a component’s dependencies. While React doesn’t include a built-in DI system like Angular, there are several effective ways to implement DI patterns in React applications.
Core DI Techniques in React
1. Prop Injection (Most Basic Form)
function UserProfile({ userService, analyticsService }) {
const [user, setUser] = useState(null);
useEffect(() => {
userService.getCurrentUser().then(user => {
setUser(user);
analyticsService.track('profile_view', { userId: user.id });
});
}, [userService, analyticsService]);
return <div>{user?.name}</div>;
}
// Usage
<UserProfile
userService={userService}
analyticsService={analyticsService}
/>
2. Context API (React’s Built-in Solution)
// Create context
const ServicesContext = createContext();
// Provider component
export function ServicesProvider({ services, children }) {
return (
<ServicesContext.Provider value={services}>
{children}
</ServicesContext.Provider>
);
}
// Custom hook for consumption
export function useService(serviceName) {
const services = useContext(ServicesContext);
if (!services) throw new Error('Missing ServicesProvider');
return services[serviceName];
}
// Usage in component
function UserProfile() {
const userService = useService('userService');
const analyticsService = useService('analyticsService');
// ... same implementation as above
}
// App setup
const services = {
userService: new UserService(),
analyticsService: new AnalyticsService()
};
function App() {
return (
<ServicesProvider services={services}>
<UserProfile />
</ServicesProvider>
);
}
3. Higher-Order Components (HOC)
function withServices(WrappedComponent, serviceNames) {
return function WithServices(props) {
const services = useContext(ServicesContext);
if (!services) throw new Error('Missing ServicesProvider');
const serviceProps = serviceNames.reduce((acc, name) => {
acc[name] = services[name];
return acc;
}, {});
return <WrappedComponent {...props} {...serviceProps} />;
};
}
// Usage
const EnhancedProfile = withServices(UserProfile, ['userService', 'analyticsService']);
Advanced DI Patterns
1. Constructor Injection with Class Components
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.userService = props.userService;
this.analyticsService = props.analyticsService;
}
componentDidMount() {
this.userService.getCurrentUser().then(user => {
this.setState({ user });
this.analyticsService.track('profile_view');
});
}
render() {
return <div>{this.state?.user?.name}</div>;
}
}
2. Function Factory Pattern
function createUserProfile({ userService, analyticsService }) {
return function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
userService.getCurrentUser().then(setUser);
}, []);
useEffect(() => {
if (user) analyticsService.track('profile_view');
}, [user]);
return <div>{user?.name}</div>;
};
}
// During app initialization
const UserProfile = createUserProfile({
userService,
analyticsService
});
// Usage in JSX
<UserProfile />
3. Dependency Injection Container
// di-container.js
class DIContainer {
constructor() {
this.services = {};
}
register(name, service) {
this.services[name] = service;
}
get(name) {
const service = this.services[name];
if (!service) throw new Error(`Service ${name} not found`);
return service;
}
}
export const container = new DIContainer();
// app-setup.js
container.register('userService', new UserService());
container.register('analyticsService', new AnalyticsService());
// In components
function UserProfile() {
const userService = container.get('userService');
const analyticsService = container.get('analyticsService');
// ... implementation
}
Testing with DI
One of the main benefits of DI is easier testing:
// Test setup
const mockUserService = {
getCurrentUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test User' })
};
const mockAnalyticsService = {
track: jest.fn()
};
test('should load and display user', async () => {
render(
<ServicesProvider services={{
userService: mockUserService,
analyticsService: mockAnalyticsService
}}>
<UserProfile />
</ServicesProvider>
);
await waitFor(() => {
expect(screen.getByText('Test User')).toBeInTheDocument();
expect(mockAnalyticsService.track).toHaveBeenCalledWith(
'profile_view',
{ userId: 1 }
);
});
});
Best Practices
- Type Safety: Use TypeScript interfaces for services
- Single Responsibility: Each service should have one purpose
- Avoid Singleton: Prefer creating new instances for tests
- Explicit Dependencies: Clearly declare all dependencies
- Layer Separation: Keep UI components unaware of service implementations
TypeScript Implementation
interface Services {
userService: UserService;
analyticsService: AnalyticsService;
// ... other services
}
const ServicesContext = createContext<Services | null>(null);
export function useService<K extends keyof Services>(name: K): Services[K] {
const services = useContext(ServicesContext);
if (!services) throw new Error('Missing ServicesProvider');
return services[name];
}
// Service interfaces
interface UserService {
getCurrentUser(): Promise<{ id: number; name: string }>;
}
interface AnalyticsService {
track(event: string, payload?: Record<string, unknown>): void;
}
// Component usage
function UserProfile() {
const userService = useService('userService');
const analyticsService = useService('analyticsService');
// ... implementation
}
Real-World Use Cases
- API Services: Inject different API clients (production vs mock)
- Analytics: Swap analytics providers easily
- Feature Flags: Inject feature toggle services
- Internationalization: Inject translation services
- Configuration: Inject environment-specific configs
Comparison with Other Patterns
Technique | Pros | Cons |
---|---|---|
Prop Injection | Simple, explicit | Prop drilling in deep trees |
Context API | No prop drilling | Less explicit dependencies |
HOC | Reusable service injection | Adds component layers |
Container | Centralized management | Global access can be abused |
Dependency Injection in React provides a clean way to manage application dependencies while maintaining testability and flexibility. The Context API offers the most React-idiomatic approach, but other patterns may be better suited depending on your specific needs.