← All Posts

5 React Native Patterns I Use on Every Project

The component, state, and architecture patterns that keep cross-platform codebases maintainable as they grow.

After shipping multiple production React Native apps, certain patterns keep showing up in every codebase I write. These aren’t clever — they’re the boring, reliable foundations that let me move fast without creating a mess.

1. Component Composition Over Configuration

The instinct when a component needs to be flexible is to add props: showHeader, footerVariant, iconLeft. That path ends with a component that has 15 props and no clear contract.

Instead, compose:

function Card({ children }: { children: React.ReactNode }) {
  return <View style={styles.card}>{children}</View>;
}

function CardHeader({ title }: { title: string }) {
  return <Text style={styles.title}>{title}</Text>;
}

// Usage — the consumer decides the structure
<Card>
  <CardHeader title="Workout Summary" />
  <MetricsRow />
</Card>

Fewer props, more flexibility, easier to test.

2. Zustand for Global State

I’ve used Redux, Context, MobX, and Jotai. I keep coming back to Zustand. It’s tiny, requires no boilerplate, and doesn’t force you into a specific data shape:

const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  signIn: async (credentials) => {
    const user = await authService.signIn(credentials);
    set({ user });
  },
  signOut: () => set({ user: null }),
}));

One store per domain. No providers to wrap. Works great with TypeScript.

3. TanStack Query for Server State

If you’re managing loading, error, and data states yourself with useState + useEffect, stop. TanStack Query handles all of it — plus caching, background refetching, and optimistic updates:

const { data: workouts, isLoading } = useQuery({
  queryKey: ["workouts", userId],
  queryFn: () => workoutService.list(userId),
  staleTime: 1000 * 60 * 5, // 5 minutes
});

The distinction between server state (what the API knows) and client state (what the UI controls) is one of the best mental models in React development.

4. Reanimated for Anything That Moves

If an animation can run on the UI thread, it should. Animated from core runs on the JS thread and will drop frames under load. react-native-reanimated runs on the UI thread and stays smooth:

const translateY = useSharedValue(0);

const gestureHandler = useAnimatedGestureHandler({
  onActive: (event) => {
    translateY.value = event.translationY;
  },
  onEnd: () => {
    translateY.value = withSpring(0);
  },
});

Once you’ve felt the difference, you won’t go back.

5. Platform Splits With File Extensions

Platform.OS === 'ios' checks scattered through your code are a smell. They’re hard to test and impossible to tree-shake. Instead, lean on Metro’s resolver:

Button.tsx          ← shared logic and types
Button.ios.tsx      ← iOS-specific render
Button.android.tsx  ← Android-specific render

The consumer just imports Button — Metro picks the right file automatically. Your platform-specific code is co-located, isolated, and easy to find.


When a feature genuinely needs platform APIs — Keychain, Android Auto, a custom camera pipeline — I reach for a Swift or Kotlin native module. But that’s the exception, not the rule. These five patterns handle the 90% case and keep the codebase sane while you get there.

What’s in your toolkit? Let me know.