Ship Maintainable React Native Apps

Ship Maintainable React Native Apps

Written by
Written by

Jaydeep A.

Post Date
Post Date

Oct 14, 2025

shares

1_0cKAyvcrXzallZWZj9KHKg

This post builds naturally on my earlier piece on building a production-ready React Native app, which focused on setting up a solid foundation before writing any UI code.

TL;DR (quick overview)

1. Navigation Best Practices

Why follow these rules?

Rules & When-To-Use

Minimal example (Auth + Main)

// navigation/RootNavigator.tsx import React from "react"; import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import AuthStack from "./AuthStack"; import MainTabs from "./MainTabs"; const Stack = createNativeStackNavigator(); export default function RootNavigator({ isAuthenticated, }: { isAuthenticated: boolean; }) { return ( <NavigationContainer> <Stack.Navigator initialRouteName={ isAuthenticated ? "Main" : "Auth"} screenOptions={{{ headerShown: false }}} > <Stack.Screen name="Auth" component={AuthStack} /> <Stack.Screen name="Main" component={MainTabs} /> </Stack.Navigator> </NavigationContainer> ); }
Why not conditional switching inside render? If you swap between entirely different navigator trees in render, you reset navigation state, break back-button expectations (Android), and harm analytics/metrics continuity.

TypeScript for Navigation: Full Type Safety

Implement comprehensive TypeScript for navigation to catch route name typos and ensure correct parameters :
// types/navigation.ts export type RootStackParamList = {   Auth: undefined; // No parameters expected   Main: undefined;   Profile: { userId: string };   Settings: { refresh?: boolean }; };
// screens/ProfileScreen.tsx import type { NativeStackScreenProps } from "@react-navigation/native-stack"; type ProfileScreenProps = NativeStackScreenProps<RootStackParamList, "Profile">; function ProfileScreen({ route, navigation }: ProfileScreenProps) {   // route.params.userId is now properly typed as string   const { userId } = route.params;   return (     <View>       <Text>User ID: {userId}</Text>     </View>   ); }
For nested navigators, use NavigatorScreenParams to maintain type safety :
import { NavigatorScreenParams } from "@react-navigation/native"; type TabParamList = {   Home: NavigatorScreenParams<StackParamList>;   Profile: { userId: string }; };

2. Component & Code Quality

High-level goal: Single Responsibility + composition over inheritance. Components should do one thing and be easy to test.

SOLID principles (practical mapping)

const { data } = useApi('users');

State management: Redux vs Context API

Rule of thumb: Start with local state + context. Introduce global stores when complexity or performance demands it.

Performance & React Native APIs You Should Know

(And yes, always read the official docs!)

InteractionManager

FlatList (for long lists)

ScrollView

Patches & patch-package

Example postinstall script

{ "scripts": { "postinstall": "patch-package" } }

3. TypeScript : Use It Wisely

Why TypeScript

Use TypeScript wisely

enum vs type vs interface

Component & HOC(Higher-Order Component) typing examples

// typed component with default props and children interface ButtonProps {   title: string;   onPress?: () => void;   disabled?: boolean;   children?: React.ReactNode; } const Button: React.FC<ButtonProps> = ({   title,   onPress,   disabled = false,   children, }) => {   return (     <TouchableOpacity onPress={onPress} disabled={disabled}>       <Text>{title}</Text>       {children}     </TouchableOpacity>   ); }; // Custom hook with TypeScript function useFetchUser(id: string) {   const [user, setUser] = useState<User | null>(null);   const [loading, setLoading] = useState(true);   useEffect(() => {     fetchUser(id)       .then(setUser)       .finally(() => setLoading(false));   }, [id]);   return { user, loading }; } // HOC(Higher-Order Component) function withBackground<P extends object>(   Wrapped: React.ComponentType<P>,   bgColor: string ) {   return (props: P & ViewProps) => (     <View style={[{ backgroundColor: bgColor }, props.style]}>       <Wrapped {...props} />     </View>   ); } // Usage of HOC const CustomText = (props: { text: string }) => <Text>{props.text}</Text>; const BlueBackgroundComponent = withBackground(CustomText, "lightblue"); export default function App() {   return (     <SafeAreaView style={{ flex: 1 }}>       <BlueBackgroundComponent text="Hello with background!" />     </SafeAreaView>   ); }

Additional TypeScript tips:

4. Styles & Color

src/ ├─ config/ │ ├─ colors.ts │ └─ fonts.ts
export const colors = {   primary: "#0A84FF",   background: "#FFFFFF",   text: {     primary: "#111111",     secondary: "#666666",   },   // semantic   success: "#28A745",   error: "#E53935", }; export type Colors = typeof colors;
export const fonts = {   regular: "Inter-Regular",   medium: "Inter-Medium",   bold: "Inter-Bold", };

Primitive → Base → Variant → Screen pattern (use only when needed)

When to use: This adds value at scale, when you have many screens and many designers. For small teams, stick to Primitives + Base components and a small set of Variants.
Example of the pattern in practice:
export const theme = {   colors: {     primary: "#0A84FF",     background: "#FFFFFF",     text: {       primary: "#111111",       secondary: "#666666",     },     semantic: {       success: "#28A745",       error: "#E53935",     },   },   spacing: {     xs: 4,     sm: 8,     md: 16,     lg: 24,     xl: 32,   },   borderRadius: {     sm: 4,     md: 8,     lg: 16,   },   typography: {     body: {       fontSize: 16,       fontFamily: "Inter-Regular",     },     heading: {       fontSize: 24,       fontFamily: "Inter-Bold",     },   }, };
// 1. Primitive components const BaseText = ({ children, style }) => { = {   return <Text style={style}>{children} </Text> };
// 2. Base components with default styles const BaseButton = ({ children, onPress, style }) => {   const theme = useTheme();   return (     <TouchableOpacity       onPress={onPress}       style={[         {           padding: theme.spacing.md,           borderRadius: theme.borderRadius.sm,           backgroundColor: theme.colors.primary,         },         style,       ]}     >       <BaseText>{children}</BaseText>     </TouchableOpacity>   ); };
// 3. Variant components const PrimaryButton = (props) => {   return <BaseButton {...props} />; }; const SecondaryButton = ({ ...props }) => {   const theme = useTheme();   return (     <BaseButton       {...props}       style={{         backgroundColor: "transparent",         borderWidth: 1,         borderColor: theme.colors.primary,       }}     />   ); };
// 4. Screen composition const ProfileScreen = () => {   return (     <View>       <PrimaryButton ondivss={() => {}}>Save Changes</PrimaryButton>       <SecondaryButton onPress={() => {}}>Cancel</SecondaryButton>     </View>   ); };
Don’t overengineer. The Primitive→Base→Variant→Screen pipeline costs cognitive overhead, use it when your app count or complexity warrants (~100 screens or cross-product design consistency).

Conclusion

Keep your navigation predictable, components single-purpose, types intentional, and styles centralized. Start small: apply colors, fonts, partition navigator files, type your public props, and only adopt the Primitive→Base→Variant pattern when your app size justifies it. These lightweight guardrails give you maintainability without the cost of premature architecture.