Maximizing Code Reuse Across Platforms
The promise of cross-platform development is code reuse. The reality is more nuanced—some code is highly reusable; some must be platform-specific.
The Reuse Pyramid
Tier 1 - Highly Reusable: Business logic, API communication, state management. A JSON decoder works the same on iOS, Android, and web. Put these in shared libraries.
Tier 2 - Partially Reusable: Validation, formatting, calculations. Mostly platform-agnostic; some platform-specific edge cases.
Tier 3 - Platform-Specific: UI, navigation, device integration. Write these per-platform, but use shared interfaces so business logic doesn't know the difference.
Shared Library Architecture
Monorepo approach: Single Git repo, shared libraries in /packages/shared/, platform-specific code in /ios/, /android/, /web/.
Example structure:
packages/
shared/ # Reusable business logic
api/ # API client
models/ # Data models
state/ # State management
react-native/ # React Native app
ios/ # iOS-specific code
android/ # Android-specific code
web/ # Web app
Dependency management: Shared packages have zero platform-specific dependencies. Platform code depends on shared code, never vice versa.
API Layer Abstraction
Create platform-agnostic API clients. Shared code calls ApiClient.get("/users/123"), platform code implements HTTP underneath.
interface IApiClient {
get(url: string, options?: RequestOptions): Promise<Response>
post(url: string, body: unknown): Promise<Response>
}
Each platform implements this interface. Unit tests run against shared code using a mock client.
State Management Reuse
Redux, Riverpod, or similar stores are platform-agnostic. Define actions, reducers, and selectors in shared code. Platform UI consumes store state.
// Shared reducer
const userReducer = (state, action) => {
if (action.type === 'SET_USER') return {...state, user: action.payload}
}
// Platform-specific UI
// Android: observes store updates
// iOS: observes store updates
// Web: observes store updates
Model and Type Definitions
Data models should be unified. In TypeScript, define interfaces in shared code. Generate types for other platforms using tools like Swagger Codegen.
// shared/models.ts
interface User {
id: string
email: string
createdAt: Date
}
Serialize consistently (ISO dates, string enums) so deserialization works across platforms.
Testing Strategy
Unit tests: Run against shared code exclusively. 100% of business logic covered.
Integration tests: Test API interaction on one platform; mirror results on others.
Platform tests: UI and navigation tested per-platform. Reuse test scenarios, not test code.
When NOT to Share
Forcing code reuse where it doesn't belong creates fragile architectures. Each platform has conventions:
- Navigation: React Navigation, Jetpack Compose, SwiftUI. Don't share navigation code.
- UI: Custom designs per platform. Shared components often look "off."
- System integration: File access, contacts, notifications. Implement per-platform.
Real-World Example
A fintech app shares authentication, transaction models, and API communication across platforms. Platform teams build platform-optimized UI and navigation using shared business logic. Shared code is 40% of the codebase; 60% is platform-specific. Slower than web-only, faster than three separate codebases.
Frequently asked questions
Should I reuse UI components across platforms?
Usually not. Platforms have different design conventions and user expectations. Reuse business logic and state management; write platform-specific UI.
What's a monorepo and why use one for cross-platform development?
A monorepo is a single Git repository containing multiple packages/apps. It enables easy sharing of code via internal packages and simplifies dependency management.
How do I structure shared code to avoid dependencies on platform-specific libraries?
Define interfaces in shared code. Platform-specific code implements those interfaces. Shared code depends on abstractions, never on platform libraries.