Dependency injection on mobile means passing dependencies in - API clients, storage, analytics - instead of importing singletons. React Native teams get testable services with factory functions, React context, and module boundaries - not Dagger, Spring, or a global service locator.
// src/shared/api/createApiClient.tsexport type ApiClient = { get<T>(path: string): Promise<T>;};export function createApiClient(baseUrl: string): ApiClient { return { async get<T>(path: string) { const response = await fetch(`${baseUrl}${path}`); if (!response.ok) throw new Error(`GET ${path} failed`); return response.json() as Promise<T>; }, };}
// src/features/orders/api/ordersApi.tsimport type { Order } from "@/entities/order";import type { ApiClient } from "@/shared/api/createApiClient";export type OrdersApi = { list(): Promise<Order[]>;};export function createOrdersApi(client: ApiClient): OrdersApi { return { list: () => client.get<Order[]>("/orders"), };}
God context with 40 services - Everything re-renders on any override change. Fix: Split by feature or use stable useMemo service objects.
Creating new service instances every render - Breaks useCallback deps and refetches endlessly. Fix: Memoize services in provider with useMemo(() => create..., [baseUrl]).
Importing concrete adapters in use cases - Inverts clean architecture. Fix: Use cases accept port interfaces; only composition root imports restAuthGateway.
jest.mock hoisting surprises - Mock path must match exact import. Fix: Prefer overrides prop on provider over module mocks when possible.
Expo Go vs dev build service URLs - Hidden __DEV__ branches in factories. Fix:getAppConfig().apiBaseUrl from app.config.ts extra per variant.
Singleton analytics in every file - Untestable event spam. Fix:AnalyticsPort injected; noop implementation in tests.
tsyringe for three services - Reflection metadata pain in Metro. Fix: Plain factories until you have 20+ wired types.
No. Factory functions plus React context cover 95% of mobile needs. Containers add indirection without solving RN-specific problems (Metro bundling, Fast Refresh).
What is the composition root?
The single place where concrete implementations bind to abstractions - typically app/_layout.tsx or AppServicesProvider. No other file should call createRestAuthGateway with production URLs.
How do I test hooks that use API clients?
Wrap with AppServicesProvider overrides={{ ordersApi: mock }}. Alternatively pass the API as a hook argument in tests only.
Context vs Zustand for services?
Context wires immutable service instances (API, storage). Zustand holds mutable app state (cart, session). Do not store fetch clients in Zustand unless they hold session tokens that change.
Can I use expo-router loaders for DI?
Expo Router does not have RSC loaders. Wire services in root layout providers - not per-route unless route-scoped deps are truly isolated.
How do environment variants get different APIs?
getAppConfig() reads extra from app.config.ts - composition root passes the correct baseUrl when creating clients. No if in feature code.
What about @tanstack/react-query?
Inject a QueryClient via context (built-in QueryClientProvider). Repository adapters feed query functions - queryFn: () => ordersApi.list().
Is jest.mock bad?
Acceptable for integration tests. Prefer provider overrides for unit tests - clearer and typed.
How do I avoid circular imports with DI?
Ports live in domain/ports.ts. Adapters import ports + entities. Composition root imports adapters. Features import ports through context - never adapters from domain.
Should SecureStore be injected?
Yes - wrap in TokenStorage port. Tests use in-memory storage; production uses createSecureTokenStorage().
Fast Refresh with context providers?
Changing provider implementation hot-reloads fine. Changing context shape may need full reload - keep AppServices type stable.
Multiple apps in monorepo - shared DI?
Shared packages/services exports factories; each apps/* composition root passes app-specific config. Do not share a singleton across apps.
When does a DI library make sense?
Rarely on RN. Consider it when porting a large backend team’s patterns or when you have 30+ wired interfaces with automated contract tests - still evaluate Metro cost first.
How does DI relate to clean architecture?
Ports are interfaces; adapters are implementations; composition root binds them. Use cases receive deps as function arguments - the purest form of injection.