Why Organization Matters in Real Apps
As a React Native app grows, maintainability becomes less about knowing individual APIs and more about keeping code easy to find, easy to change, and hard to break. “Organization” is not just folder aesthetics: it affects onboarding time, bug-fix speed, feature delivery, and how confidently you can refactor. A maintainable structure makes it clear where a piece of UI lives, where its styles belong, how it gets data, and how navigation is wired—without forcing you to open ten files to understand one screen.
This chapter focuses on practical patterns for organizing components, styles, and navigation so that features can evolve independently. The goal is to reduce coupling (one change causing many unrelated changes), avoid duplication, and keep responsibilities clear.
A Feature-Oriented Folder Structure
A common scaling problem is grouping code only by “type” (all components in one folder, all screens in another). That can work for small apps, but it often leads to long import paths and unclear ownership. A feature-oriented structure groups related screens, components, hooks, and styles together by domain (for example, auth, profile, settings). Shared pieces still exist, but they are clearly labeled as shared.
Example folder layout
src/ ├─ app/ │ ├─ navigation/ │ │ ├─ RootNavigator.tsx │ │ ├─ linking.ts │ │ └─ types.ts │ ├─ providers/ │ │ ├─ AppProviders.tsx │ │ └─ theme/ │ │ ├─ ThemeProvider.tsx │ │ ├─ tokens.ts │ │ └─ useTheme.ts │ └─ config/ │ └─ env.ts ├─ features/ │ ├─ auth/ │ │ ├─ screens/ │ │ │ ├─ LoginScreen.tsx │ │ │ └─ SignupScreen.tsx │ │ ├─ components/ │ │ │ ├─ AuthHeader.tsx │ │ │ └─ PasswordField.tsx │ │ ├─ hooks/ │ │ │ └─ useAuthForm.ts │ │ └─ navigation/ │ │ └─ AuthNavigator.tsx │ ├─ profile/ │ │ ├─ screens/ │ │ ├─ components/ │ │ └─ navigation/ │ │ └─ ProfileNavigator.tsx │ └─ settings/ │ ├─ screens/ │ ├─ components/ │ └─ navigation/ │ └─ SettingsNavigator.tsx ├─ shared/ │ ├─ components/ │ │ ├─ Button/ │ │ │ ├─ Button.tsx │ │ │ ├─ styles.ts │ │ │ └─ index.ts │ │ ├─ Text/ │ │ └─ Screen/ │ ├─ styles/ │ │ ├─ spacing.ts │ │ ├─ typography.ts │ │ └─ shadows.ts │ ├─ utils/ │ └─ hooks/ └─ index.tsThis structure makes a few things explicit:
- Features own their UI: screens and feature-specific components live together.
- Shared is intentional: anything in
shared/should be reusable across multiple features. - Navigation is modular: each feature can define its own navigator, then the root navigator composes them.
Defining Boundaries: Screen vs Feature Component vs Shared Component
Maintainability improves when each file has a clear role. A helpful mental model is three layers:
- Listen to the audio with the screen off.
- Earn a certificate upon completion.
- Over 5000 courses for you to explore!
Download the app
- Screens: route-level components. They connect navigation, orchestrate data fetching, and assemble feature components.
- Feature components: UI pieces used only inside a feature (for example,
AuthHeaderused only in auth screens). - Shared components: generic building blocks used across features (for example,
Button,Text,Screenwrappers).
Practical rule of thumb
- If a component is used in two or more features, consider moving it to
shared/components. - If it depends on feature-specific wording, data shapes, or business rules, keep it inside the feature.
- If it is a screen, keep it thin: compose components and call hooks rather than implementing everything inline.
Organizing Styles for Consistency and Reuse
As apps grow, style duplication becomes a major source of inconsistency. Two buttons that look “almost” the same are a maintenance trap. A scalable approach is to combine:
- Design tokens (colors, spacing, typography scales) stored centrally.
- Component-level styles stored next to the component.
- Shared style helpers for common patterns (for example, shadows, spacing constants).
Step-by-step: introduce design tokens
1) Create a central tokens file (or theme object) that contains primitives. Keep it boring and predictable.
// src/app/providers/theme/tokens.ts export const colors = { background: '#FFFFFF', text: '#111827', mutedText: '#6B7280', primary: '#2563EB', danger: '#DC2626', border: '#E5E7EB', }; export const spacing = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, xxl: 32, }; export const radius = { sm: 8, md: 12, lg: 16, }; export const typography = { title: { fontSize: 24, fontWeight: '700' as const }, body: { fontSize: 16, fontWeight: '400' as const }, caption: { fontSize: 13, fontWeight: '400' as const }, };2) Use tokens in component styles instead of hard-coded values. This makes global adjustments easy (for example, changing base spacing or brand color).
// src/shared/components/Button/styles.ts import { StyleSheet } from 'react-native'; import { colors, spacing, radius } from '../../../app/providers/theme/tokens'; export const styles = StyleSheet.create({ base: { paddingVertical: spacing.sm, paddingHorizontal: spacing.lg, borderRadius: radius.md, alignItems: 'center', justifyContent: 'center', }, primary: { backgroundColor: colors.primary, }, danger: { backgroundColor: colors.danger, }, label: { color: '#FFFFFF', fontWeight: '600', }, });3) Keep component styles close to the component. This reduces “style hunting” and encourages encapsulation.
When to create shared style modules
Shared style modules are useful when you see repeated patterns that are not full components. Examples include consistent screen padding, card shadows, or typography presets. Keep them small and composable.
// src/shared/styles/shadows.ts export const shadows = { card: { shadowColor: '#000', shadowOpacity: 0.08, shadowRadius: 10, shadowOffset: { width: 0, height: 4 }, elevation: 3, }, };Component Packaging: Index Files and Co-Located Modules
To keep imports clean and reduce accidental coupling, package components in their own folders when they have multiple files (component, styles, tests, story/demo, helpers). Export a single public entry point.
Example: a shared Button component package
// src/shared/components/Button/index.ts export { Button } from './Button'; export type { ButtonProps } from './Button';// src/shared/components/Button/Button.tsx import React from 'react'; import { Pressable, Text, ActivityIndicator } from 'react-native'; import { styles } from './styles'; export type ButtonProps = { label: string; onPress: () => void; variant?: 'primary' | 'danger'; loading?: boolean; disabled?: boolean; }; export function Button({ label, onPress, variant = 'primary', loading, disabled }: ButtonProps) { const isDisabled = disabled || loading; return ( <Pressable onPress={onPress} disabled={isDisabled} style={[styles.base, styles[variant], isDisabled && { opacity: 0.6 }]} > {loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.label}>{label}</Text>} </Pressable> ); }This pattern makes it obvious what is public (exports from index.ts) and what is internal (other files). It also reduces long relative imports because consumers import from the folder.
Navigation Organization: Root Composition and Feature Navigators
In maintainable apps, navigation should be composed rather than centralized into one giant file. A practical approach is:
- Each feature defines its own navigator (and route types) for screens it owns.
- The root navigator composes feature navigators and handles cross-cutting flows (for example, authenticated vs unauthenticated).
- Navigation types are centralized so route params remain consistent and refactors are safe.
Step-by-step: create a navigation “types” module
Keep route names and param types in one place so screens and navigators agree.
// src/app/navigation/types.ts export type AuthStackParamList = { Login: undefined; Signup: { email?: string } | undefined; }; export type ProfileStackParamList = { ProfileHome: undefined; EditProfile: { userId: string }; }; export type RootStackParamList = { Auth: undefined; Main: undefined; };Feature navigator example
// src/features/auth/navigation/AuthNavigator.tsx import React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import type { AuthStackParamList } from '../../../app/navigation/types'; import { LoginScreen } from '../screens/LoginScreen'; import { SignupScreen } from '../screens/SignupScreen'; const Stack = createNativeStackNavigator<AuthStackParamList>(); export function AuthNavigator() { return ( <Stack.Navigator> <Stack.Screen name="Login" component={LoginScreen} /> <Stack.Screen name="Signup" component={SignupScreen} /> </Stack.Navigator> ); }Root navigator composes features
// src/app/navigation/RootNavigator.tsx import React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import type { RootStackParamList } from './types'; import { AuthNavigator } from '../../features/auth/navigation/AuthNavigator'; import { MainNavigator } from './MainNavigator'; const Stack = createNativeStackNavigator<RootStackParamList>(); export function RootNavigator() { const isSignedIn = false; return ( <Stack.Navigator screenOptions={{ headerShown: false }}> {isSignedIn ? ( <Stack.Screen name="Main" component={MainNavigator} /> ) : ( <Stack.Screen name="Auth" component={AuthNavigator} /> )} </Stack.Navigator> ); }The key maintainability win is that auth screens can change without touching unrelated navigation code. The root only decides which flow to show.
Keeping Navigation Details Out of UI Components
A common maintainability issue is deeply nested components calling navigation directly. This couples UI to routing and makes components harder to reuse. Prefer these patterns:
- Screen owns navigation: the screen passes callbacks to children.
- Feature-level “container” component: if a screen is too large, create a feature container that handles navigation and data, then renders presentational components.
Example: screen passes an intent callback
// src/features/profile/screens/ProfileHomeScreen.tsx import React from 'react'; import { View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { ProfileStackParamList } from '../../../app/navigation/types'; import { ProfileHeader } from '../components/ProfileHeader'; type Nav = NativeStackNavigationProp<ProfileStackParamList, 'ProfileHome'>; export function ProfileHomeScreen() { const navigation = useNavigation<Nav>(); return ( <View> <ProfileHeader onEditProfile={(userId) => navigation.navigate('EditProfile', { userId })} /> </View> ); }// src/features/profile/components/ProfileHeader.tsx import React from 'react'; import { View, Text, Pressable } from 'react-native'; export function ProfileHeader({ onEditProfile }: { onEditProfile: (userId: string) => void }) { return ( <View> <Text>Your Profile</Text> <Pressable onPress={() => onEditProfile('u_123')}> <Text>Edit</Text> </Pressable> </View> ); }ProfileHeader stays reusable and testable because it doesn’t know about route names or navigation objects.
Shared Screen Wrappers for Consistent Layout
Many apps repeat the same screen scaffolding: safe area handling, background color, default padding, and optional scroll behavior. Instead of repeating that logic in every screen, create a shared Screen component that standardizes layout and reduces style drift.
Step-by-step: build a Screen wrapper
1) Create a shared component that supports common options (scrollable, padded, background).
// src/shared/components/Screen/Screen.tsx import React from 'react'; import { View, ScrollView } from 'react-native'; import { colors, spacing } from '../../../app/providers/theme/tokens'; type ScreenProps = { children: React.ReactNode; scroll?: boolean; padded?: boolean; }; export function Screen({ children, scroll, padded = true }: ScreenProps) { const content = ( <View style={{ flex: 1, backgroundColor: colors.background, padding: padded ? spacing.lg : 0 }}> {children} </View> ); return scroll ? <ScrollView contentContainerStyle={{ flexGrow: 1 }}>{content}</ScrollView> : content; }2) Use it in screens to reduce boilerplate and enforce consistency.
// src/features/settings/screens/SettingsScreen.tsx import React from 'react'; import { Text } from 'react-native'; import { Screen } from '../../../shared/components/Screen'; export function SettingsScreen() { return ( <Screen> <Text>Settings</Text> </Screen> ); }Managing Imports and Avoiding Dependency Tangles
As the codebase grows, circular dependencies and messy imports become common. A few practical rules help:
- One-way dependencies: features can depend on
shared, butsharedshould not depend on features. - Navigation depends on screens, not the other way around: screens should not import navigators.
- Prefer absolute imports (with a configured alias) to avoid fragile relative paths like
../../../../.
Example: barrel exports for shared components
// src/shared/components/index.ts export { Button } from './Button'; export { Screen } from './Screen';Then screens can import cleanly:
import { Screen, Button } from '../../shared/components';Practical Refactor: From “Big Screen File” to Maintainable Modules
When a screen grows, it often accumulates UI, styles, and helper functions in one file. Refactoring into modules can be done incrementally without changing behavior.
Step-by-step refactor checklist
1) Extract presentational sections: move repeated or visually distinct blocks into feature components (for example, OrderSummary, PaymentMethodCard).
2) Extract styles: move StyleSheet.create into a styles.ts file next to the component. Keep naming consistent (for example, container, title, row).
3) Extract logic into hooks: if the screen has complex derived state or event handlers, create a feature hook like useCheckout that returns the data and actions the UI needs.
4) Keep navigation at the top: the screen should translate user intents into navigation actions and pass callbacks down.
5) Promote truly reusable pieces: if a component becomes useful in multiple features, move it to shared and ensure it has generic props.
Example: extracting a hook and styles
// src/features/auth/hooks/useAuthForm.ts import { useMemo, useState } from 'react'; export function useAuthForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const canSubmit = useMemo(() => email.includes('@') && password.length >= 8, [email, password]); return { email, setEmail, password, setPassword, canSubmit }; }// src/features/auth/screens/LoginScreen.tsx import React from 'react'; import { TextInput, Text } from 'react-native'; import { Screen } from '../../../shared/components/Screen'; import { Button } from '../../../shared/components/Button'; import { useAuthForm } from '../hooks/useAuthForm'; import { styles } from './styles'; export function LoginScreen() { const { email, setEmail, password, setPassword, canSubmit } = useAuthForm(); return ( <Screen> <Text style={styles.title}>Log in</Text> <TextInput value={email} onChangeText={setEmail} style={styles.input} placeholder="Email" /> <TextInput value={password} onChangeText={setPassword} style={styles.input} placeholder="Password" secureTextEntry /> <Button label="Continue" onPress={() => {}} disabled={!canSubmit} /> </Screen> ); }// src/features/auth/screens/styles.ts import { StyleSheet } from 'react-native'; import { colors, spacing, typography } from '../../../app/providers/theme/tokens'; export const styles = StyleSheet.create({ title: { ...typography.title, color: colors.text, marginBottom: spacing.lg, }, input: { borderWidth: 1, borderColor: colors.border, borderRadius: 10, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, marginBottom: spacing.md, }, });This refactor improves readability: the screen reads like a layout, while logic and styles live in dedicated modules.
Naming Conventions That Prevent Confusion
Consistent naming reduces cognitive load. A few conventions that scale well:
- Component files:
PascalCase.tsx(for example,LoginScreen.tsx,PasswordField.tsx). - Hooks:
useSomething.ts(for example,useAuthForm.ts). - Styles:
styles.tsnext to the component, orComponentName.styles.tsif you prefer explicitness. - Navigator files:
FeatureNavigator.tsx(for example,AuthNavigator.tsx). - Route names: stable strings that match screen names; avoid renaming frequently.
Practical Guardrails for Long-Term Maintainability
Organization is easier to keep than to rebuild. These guardrails help prevent gradual decay:
- Limit shared components: shared should contain primitives and truly generic UI. If shared becomes a dumping ground, it becomes hard to change safely.
- Prefer composition over configuration: instead of a single “mega component” with dozens of props, compose smaller components. This keeps APIs understandable.
- Keep feature boundaries strong: avoid importing one feature’s internal components from another feature. If something must be reused, promote it to shared.
- Centralize tokens, not every style: tokens should be global; component styles should remain local. This balance prevents a giant global stylesheet.
- Document public APIs with types: even in JavaScript projects, using clear prop shapes and consistent naming makes components easier to adopt correctly.