D Dependency Inversion Principle DIP · Quick recall Q&A

2 min read
Mid-level3 min read
Rapid 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:
  1. Single use case → Use concrete class directly
  2. Need tests → Extract interface, inject via props
  3. 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.