Back to Posts

State Management in React Native 2025

By Lumina Software
react-nativestate-managementtypescriptbest-practices

State Management in React Native 2025

State management in React Native has matured significantly. With new patterns, libraries, and React 19 features, here's your guide to managing state effectively in 2025.

The State Management Landscape

When to Use What

Local State (useState): Component-specific UI state Context API: App-wide simple state Zustand: Lightweight global state TanStack Query: Server state and caching Redux Toolkit: Complex state with time-travel debugging Jotai/Recoil: Atomic state management

Modern Patterns

1. Server State vs Client State

The key insight: treat server data differently from client state.

// ❌ Don't mix server and client state
const [users, setUsers] = useState([]);
useEffect(() => {
  fetchUsers().then(setUsers);
}, []);

// ✅ Separate concerns
// Server state with TanStack Query
const { data: users } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
});

// Client state with useState
const [selectedUserId, setSelectedUserId] = useState(null);

2. Zustand for Global State

Zustand has become the go-to for simple global state:

import create from 'zustand';
import { persist } from 'zustand/middleware';

interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  setUser: (user: User) => void;
  setTheme: (theme: 'light' | 'dark') => void;
}

export const useAppStore = create<AppState>()(
  persist(
    (set) => ({
      user: null,
      theme: 'light',
      setUser: (user) => set({ user }),
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'app-storage',
    }
  )
);

// Usage in components
function Profile() {
  const user = useAppStore((state) => state.user);
  const setUser = useAppStore((state) => state.setUser);
  
  return <Text>{user?.name}</Text>;
}

3. TanStack Query for Server State

Perfect for API data:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Fetching data
function PostsList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
  
  if (isLoading) return <Loading />;
  if (error) return <Error message={error.message} />;
  
  return <FlatList data={data} renderItem={renderPost} />;
}

// Mutations
function CreatePost() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: createPost,
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });
  
  return (
    <Button
      onPress={() => mutation.mutate({ title: 'New Post' })}
      disabled={mutation.isPending}
    >
      Create Post
    </Button>
  );
}

4. Optimistic Updates

Update UI immediately, rollback on error:

const mutation = useMutation({
  mutationFn: updatePost,
  onMutate: async (newPost) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['posts'] });
    
    // Snapshot previous value
    const previousPosts = queryClient.getQueryData(['posts']);
    
    // Optimistically update
    queryClient.setQueryData(['posts'], (old) => [
      ...old,
      { ...newPost, id: Date.now() },
    ]);
    
    return { previousPosts };
  },
  onError: (err, newPost, context) => {
    // Rollback on error
    queryClient.setQueryData(['posts'], context.previousPosts);
  },
  onSettled: () => {
    // Refetch to ensure consistency
    queryClient.invalidateQueries({ queryKey: ['posts'] });
  },
});

React 19 Features

use() Hook

Handle promises and context more elegantly:

import { use } from 'react';

function UserProfile({ userPromise }) {
  const user = use(userPromise);
  return <Text>{user.name}</Text>;
}

useOptimistic

Optimistic updates built into React:

import { useOptimistic } from 'react';

function TaskList({ tasks }) {
  const [optimisticTasks, addOptimisticTask] = useOptimistic(
    tasks,
    (state, newTask) => [...state, { ...newTask, pending: true }]
  );
  
  async function handleAdd(task) {
    addOptimisticTask(task);
    await createTask(task);
  }
  
  return (
    <FlatList
      data={optimisticTasks}
      renderItem={({ item }) => (
        <TaskCard task={item} isPending={item.pending} />
      )}
    />
  );
}

State Management Patterns

1. Colocation

Keep state close to where it's used:

// ✅ Good: State colocated with component
function TaskCard({ task }) {
  const [isExpanded, setIsExpanded] = useState(false);
  // Only this component needs expansion state
  return <View>...</View>;
}

// ❌ Bad: Unnecessary global state
const useExpandedTasks = create((set) => ({
  expanded: new Set(),
  toggle: (id) => set((state) => ({
    expanded: new Set(state.expanded).toggle(id),
  })),
}));

2. Derived State

Compute values instead of storing them:

// ❌ Bad: Storing derived state
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// ✅ Good: Derive when needed
const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);

3. Normalized State

Keep data normalized for easier updates:

// ❌ Bad: Nested structure
const state = {
  posts: [
    { id: 1, author: { id: 1, name: 'John' } },
    { id: 2, author: { id: 1, name: 'John' } },
  ],
};

// ✅ Good: Normalized
const state = {
  posts: {
    1: { id: 1, authorId: 1 },
    2: { id: 2, authorId: 1 },
  },
  authors: {
    1: { id: 1, name: 'John' },
  },
};

Performance Optimization

Selective Subscriptions

Only subscribe to what you need:

// ❌ Bad: Subscribes to entire store
const store = useAppStore();

// ✅ Good: Selective subscription
const user = useAppStore((state) => state.user);

Memoization

Prevent unnecessary re-renders:

const expensiveValue = useMemo(
  () => computeExpensiveValue(data),
  [data]
);

const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

Testing State Management

Testing Zustand Stores

import { renderHook, act } from '@testing-library/react';
import { useAppStore } from './store';

test('sets user', () => {
  const { result } = renderHook(() => useAppStore());
  
  act(() => {
    result.current.setUser({ id: 1, name: 'John' });
  });
  
  expect(result.current.user).toEqual({ id: 1, name: 'John' });
});

Testing TanStack Query

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } },
});

test('fetches posts', async () => {
  const wrapper = ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
  
  const { result } = renderHook(() => usePosts(), { wrapper });
  
  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toHaveLength(10);
});

Common Mistakes

  1. Over-engineering: Using Redux for simple state
  2. Mixing concerns: Server state in client state management
  3. Not normalizing: Nested data structures that are hard to update
  4. Ignoring caching: Refetching data unnecessarily
  5. Not handling loading/error states: Poor UX

Recommended Stack for 2025

Small Apps:

  • Local state: useState
  • Global state: Zustand
  • Server state: TanStack Query

Medium Apps:

  • Local state: useState
  • Global state: Zustand + Context API
  • Server state: TanStack Query
  • Form state: React Hook Form

Large Apps:

  • Local state: useState
  • Global state: Redux Toolkit or Zustand
  • Server state: TanStack Query
  • Form state: React Hook Form
  • URL state: Expo Router

Conclusion

State management in 2025 is about choosing the right tool for the job:

  • Local state for component UI
  • Zustand for simple global state
  • TanStack Query for server state
  • React 19 features for better UX

The key is understanding your needs and picking tools that match. Don't overcomplicate, but don't under-engineer either.