D Dependency Inversion Principle DIP · Quick recall Q&A
2 min readRapid overview
Quick recall Q&A
Q: How does DIP differ from simple dependency injection in React? A: DIP is the principle (depend on abstractions). Dependency injection is a technique to supply those abstractions at runtime. You can inject dependencies via props, hooks, or Context, but DIP guides the design to use interfaces/abstractions.
Q: What is the composition root in React, and why is it important? A: It's typically the top-level App component or a provider tree where concrete implementations are created and injected. Keeping all bindings there ensures the rest of the system depends only on abstractions, honoring DIP.
// Production
<UserProfile repository={new ApiUserRepository()} />
// Tests
<UserProfile repository={new MockUserRepository()} />
Q: How does DIP help with testing React components? A: You can swap real implementations with mocks when components depend on interfaces:
Tests stay fast and deterministic.
- You need to swap implementations
- Testing requires mocks
- Multiple implementations exist
Q: When should you avoid introducing an interface in TypeScript? A: If there's only one implementation with no foreseeable variation, an interface may add noise. Start with concrete classes and extract interfaces when:
// Bad - hook creates concrete dependency
function useUser(userId: string) {
const api = new ApiClient(); // ❌ Hardcoded
}
// Good - hook receives abstraction
function useUser(api: UserApi, userId: string) {
// Can inject MockUserApi for tests
}
Q: How does DIP interact with React hooks? A: Custom hooks should accept dependencies rather than creating them:
// interfaces/storage.ts - stable abstraction
export interface StorageService {
save(key: string, value: string): void;
load(key: string): string | null;
}
// implementations/localStorage.ts - concrete detail
import { StorageService } from '../interfaces/storage';
export class LocalStorageService implements StorageService { ... }
Q: How do you keep abstractions stable in frontend code? A: Define them in separate files/modules and keep them small:
// Bad - couples to Firebase
const user = useContext(FirebaseAuthContext);
// Good - couples to abstraction
const authService = useContext(AuthServiceContext); // Can be any AuthService
Q: What's wrong with useContext without abstraction? A: It couples components to specific implementations:
const ApiContext = createContext<ApiClient | null>(null);
// Provide concrete implementation
<ApiContext.Provider value={new HttpApiClient()}>
<App />
</ApiContext.Provider>
// Components consume abstraction
const api = useContext(ApiContext);
Q: How do you manage React Context for DI? A: Create typed contexts for interfaces, not concrete classes:
const authService = useFeatureFlag('new-auth')
? new Auth0Service()
: new FirebaseAuthService();
<AuthProvider service={authService}>
<App />
</AuthProvider>
Q: How does DIP help with feature flags in React? A: You can conditionally provide different implementations:
// Abstract token
export abstract class StorageService {
abstract save(key: string, value: string): void;
abstract load(key: string): string | null;
}
// Provide implementation
providers: [
{ provide: StorageService, useClass: LocalStorageService }
]
// Component injects abstraction
constructor(private storage: StorageService) {}
Q: How do you enforce DIP in Angular? A: Use abstract classes as DI tokens:
interface UserStore {
getUser(id: string): User | null;
updateUser(id: string, data: Partial<User>): void;
}
// Redux implementation
class ReduxUserStore implements UserStore { ... }
// Zustand implementation
class ZustandUserStore implements UserStore { ... }
// Components depend on interface
function UserProfile({ store }: { store: UserStore }) { ... }
Q: How does DIP apply to state management (Redux/Zustand)? A: Define action creators and selectors as interfaces, inject store implementation:
const apiClient = process.env.NODE_ENV === 'production'
? new ProductionApiClient()
: new MockApiClient();
<ApiProvider client={apiClient}>
<App />
</ApiProvider>
Q: How does DIP help with environment-specific code? A: Inject different implementations based on environment:
render(<Component api={mockApi} />); // Inject mock via DI
// Test behavior, not implementation details
Q: What's the relationship between DIP and testing library best practices? A: Testing Library encourages testing behavior over implementation. DIP supports this by allowing you to inject test doubles while keeping component logic unchanged:
- Single use case → Use concrete class directly
- Need tests → Extract interface, inject via props
- Multiple implementations → Full DIP with Context/providers
Q: How do you balance DIP with simplicity in React? A: Start simple, add abstractions when needed:
Don't over-abstract prematurely.