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?

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?

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.

Q: How does DIP help with testing React components?

You can swap real implementations with mocks when components depend on interfaces:

// Production
<UserProfile repository={new ApiUserRepository()} />

// Tests
<UserProfile repository={new MockUserRepository()} />

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?

If there's only one implementation with no foreseeable variation, an interface may add noise. Start with concrete classes and extract interfaces when:

Q: How does DIP interact with React hooks?

Custom hooks should accept dependencies rather than creating them:

// 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 do you keep abstractions stable in frontend code?

Define them in separate files/modules and keep them small:

// 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: What's wrong with useContext without abstraction?

It couples components to specific implementations:

// Bad - couples to Firebase
const user = useContext(FirebaseAuthContext);

// Good - couples to abstraction
const authService = useContext(AuthServiceContext); // Can be any AuthService
Q: How do you manage React Context for DI?

Create typed contexts for interfaces, not concrete classes:

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 does DIP help with feature flags in React?

You can conditionally provide different implementations:

const authService = useFeatureFlag('new-auth')
  ? new Auth0Service()
  : new FirebaseAuthService();

<AuthProvider service={authService}>
  <App />
</AuthProvider>
Q: How do you enforce DIP in Angular?

Use abstract classes as DI tokens:

// 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 does DIP apply to state management (Redux/Zustand)?

Define action creators and selectors as interfaces, inject store implementation:

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 help with environment-specific code?

Inject different implementations based on environment:

const apiClient = process.env.NODE_ENV === 'production'
  ? new ProductionApiClient()
  : new MockApiClient();

<ApiProvider client={apiClient}>
  <App />
</ApiProvider>
Q: What's the relationship between DIP and testing library best practices?

Testing Library encourages testing behavior over implementation. DIP supports this by allowing you to inject test doubles while keeping component logic unchanged:

render(<Component api={mockApi} />); // Inject mock via DI
// Test behavior, not implementation details
  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?

Start simple, add abstractions when needed:

Don't over-abstract prematurely.