Starting with the Right Foundation
The architectural decisions you make in the first week of a React Native project determine how maintainable, testable, and scalable the app will be for years to come. Getting navigation, state management, and project structure right from the start saves enormous refactoring effort later.
At StrikingWeb, we have built React Native apps for e-commerce, healthcare, and fintech clients. This guide reflects the patterns and practices that have proven most effective across those projects.
Project Setup and Tooling
We recommend using the React Native CLI (rather than Expo) for production applications. While Expo simplifies initial setup, it limits access to native modules and complicates the build process for apps that need custom native functionality.
# Initialize a new React Native project with TypeScript
npx react-native init MyApp --template react-native-template-typescript
# Project structure
MyApp/
src/
api/ # API client and request functions
assets/ # Images, fonts, and static files
components/ # Reusable UI components
hooks/ # Custom React hooks
navigation/ # Navigation configuration
screens/ # Screen components
store/ # State management
theme/ # Colors, typography, spacing
types/ # TypeScript type definitions
utils/ # Helper functions
App.tsx
TypeScript is non-negotiable for React Native projects. The combination of complex navigation types, API response shapes, and component props creates exactly the kind of codebase where type safety prevents the most bugs.
Navigation Architecture
React Navigation is the standard navigation library for React Native. It provides stack, tab, and drawer navigators that compose together to build complex navigation structures. The key is designing your navigation hierarchy before writing screen components.
Typical Navigation Structure
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
// Define the navigation parameter types
type RootStackParamList = {
Auth: undefined;
Main: undefined;
};
type AuthStackParamList = {
Login: undefined;
Register: undefined;
ForgotPassword: { email?: string };
};
type MainTabParamList = {
Home: undefined;
Search: undefined;
Cart: undefined;
Profile: undefined;
};
type HomeStackParamList = {
HomeScreen: undefined;
ProductDetail: { productId: string };
Category: { categoryId: string; title: string };
};
const RootStack = createNativeStackNavigator<RootStackParamList>();
const AuthStack = createNativeStackNavigator<AuthStackParamList>();
const MainTab = createBottomTabNavigator<MainTabParamList>();
const HomeStack = createNativeStackNavigator<HomeStackParamList>();
function App() {
const { isAuthenticated } = useAuth();
return (
<NavigationContainer>
<RootStack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<RootStack.Screen name="Main" component={MainNavigator} />
) : (
<RootStack.Screen name="Auth" component={AuthNavigator} />
)}
</RootStack.Navigator>
</NavigationContainer>
);
}
The root navigator switches between authentication and main flows based on login state. The main flow uses a tab navigator with nested stack navigators for each tab. This pattern supports deep linking, handles back navigation correctly, and keeps each tab's navigation state independent.
State Management
State management in React Native is the same as in React web applications, with the added consideration of offline support and persistence. We evaluate state management needs in layers:
Local Component State
Use useState and useReducer for state that belongs to a single component or a small component tree. Form inputs, modal visibility, and animation values are local state.
Server State
For data fetched from APIs, we use React Query (TanStack Query). It handles caching, background refetching, optimistic updates, and loading/error states out of the box. This eliminates the enormous amount of boilerplate code that teams typically write for API data management:
import { useQuery, useMutation, useQueryClient } from 'react-query';
import { api } from '../api/client';
// Fetch products with automatic caching and refetching
function useProducts(categoryId: string) {
return useQuery(
['products', categoryId],
() => api.getProducts(categoryId),
{
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
cacheTime: 30 * 60 * 1000, // Keep in cache for 30 minutes
}
);
}
// Add to cart with optimistic update
function useAddToCart() {
const queryClient = useQueryClient();
return useMutation(
(item: CartItem) => api.addToCart(item),
{
onMutate: async (newItem) => {
await queryClient.cancelQueries('cart');
const previousCart = queryClient.getQueryData('cart');
queryClient.setQueryData('cart', (old: Cart) => ({
...old,
items: [...old.items, newItem],
}));
return { previousCart };
},
onError: (err, newItem, context) => {
queryClient.setQueryData('cart', context.previousCart);
},
onSettled: () => {
queryClient.invalidateQueries('cart');
},
}
);
}
Global Application State
For truly global state — user authentication, app-wide settings, theme preferences — we use Zustand for its simplicity and minimal boilerplate:
import create from 'zustand';
import { persist } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthState {
token: string | null;
user: User | null;
isAuthenticated: boolean;
login: (token: string, user: User) => void;
logout: () => void;
}
const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
isAuthenticated: false,
login: (token, user) => set({ token, user, isAuthenticated: true }),
logout: () => set({ token: null, user: null, isAuthenticated: false }),
}),
{
name: 'auth-storage',
getStorage: () => AsyncStorage,
}
)
);
API Integration
Create a centralized API client that handles authentication headers, error handling, and request/response transformation:
import axios from 'axios';
import { useAuthStore } from '../store/auth';
const apiClient = axios.create({
baseURL: 'https://api.example.com/v1',
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
// Add auth token to every request
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle token expiry globally
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
export const api = {
getProducts: (categoryId: string) =>
apiClient.get(`/products?category=${categoryId}`).then(r => r.data),
getProduct: (id: string) =>
apiClient.get(`/products/${id}`).then(r => r.data),
addToCart: (item: CartItem) =>
apiClient.post('/cart/items', item).then(r => r.data),
};
Component Architecture
Organize components into two categories: screen components and reusable UI components. Screen components correspond to navigation screens and contain the business logic for that view. UI components are pure, reusable building blocks that receive data through props.
Building a Consistent Design System
Define your theme centrally and reference it throughout the app:
// theme/index.ts
export const theme = {
colors: {
primary: '#3B82F6',
secondary: '#6366F1',
success: '#10B981',
error: '#EF4444',
background: '#FFFFFF',
surface: '#F9FAFB',
text: '#111827',
textSecondary: '#6B7280',
border: '#E5E7EB',
},
spacing: {
xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48,
},
typography: {
h1: { fontSize: 28, fontWeight: '700' as const, lineHeight: 36 },
h2: { fontSize: 22, fontWeight: '600' as const, lineHeight: 28 },
body: { fontSize: 16, fontWeight: '400' as const, lineHeight: 24 },
caption: { fontSize: 12, fontWeight: '400' as const, lineHeight: 16 },
},
borderRadius: {
sm: 4, md: 8, lg: 12, full: 9999,
},
};
Performance Optimization
- FlatList for lists: Never use ScrollView for long lists. FlatList virtualizes off-screen items and only renders what is visible.
- Memoization: Use
React.memo,useMemo, anduseCallbackto prevent unnecessary re-renders, especially for list item components. - Image optimization: Use FastImage (a replacement for the built-in Image component) for caching and progressive loading. Serve appropriately sized images for different screen densities.
- Avoid anonymous functions in JSX: Anonymous functions create new references on every render, causing child components to re-render unnecessarily.
- Hermes engine: Enable the Hermes JavaScript engine for faster startup time and lower memory usage. It is now the default in React Native 0.70+.
Testing Strategy
- Unit tests: Jest for testing business logic, utility functions, and custom hooks
- Component tests: React Native Testing Library for testing component rendering and user interactions
- Integration tests: Detox or Maestro for end-to-end testing on real devices and simulators
Focus testing effort on business-critical flows: authentication, checkout, and data-sensitive operations. Component tests should verify behavior (what happens when the user taps the button), not implementation details (which state variable changed).
Build and Distribution
For production builds, we use Fastlane to automate the build, signing, and distribution process for both iOS and Android. Combined with a CI/CD service like GitHub Actions or CircleCI, this creates a pipeline where pushing a tag to the repository automatically builds the app, runs tests, and uploads to TestFlight (iOS) or Google Play Console (Android).
The best React Native apps are the ones where the architecture scales with the team. A well-structured codebase allows new developers to be productive within days, features to be added without fear of breaking existing functionality, and the app to grow from a few screens to dozens without becoming unmaintainable.
Ready to Build?
Building a React Native app from scratch requires decisions about navigation, state management, API design, and testing that will shape the project for its lifetime. At StrikingWeb, we bring battle-tested architectural patterns and production experience to every mobile project. If you are planning a React Native app, we would love to discuss your requirements and help you build on a solid foundation.