Organizing Components, Styles, and Navigation for Maintainable Apps

Capítulo 11

Estimated reading time: 12 minutes

+ Exercise

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.ts

This 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:

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

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, AuthHeader used only in auth screens).
  • Shared components: generic building blocks used across features (for example, Button, Text, Screen wrappers).

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, but shared should 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.ts next to the component, or ComponentName.styles.ts if 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.

Now answer the exercise about the content:

Which approach best reduces coupling between UI components and routing when building maintainable React Native screens?

You are right! Congratulations, now go to the next page

You missed! Try again.

Keeping navigation at the screen (or a container) and passing callbacks down makes UI components reusable and testable because they do not depend on route names or navigation objects.

Next chapter

Debugging UI and State Issues with Practical Troubleshooting Habits

Arrow Right Icon
Free Ebook cover React Native Basics: Components, Styling, and Navigation Concepts
92%

React Native Basics: Components, Styling, and Navigation Concepts

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.