State Management in React Native 2025
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
- Over-engineering: Using Redux for simple state
- Mixing concerns: Server state in client state management
- Not normalizing: Nested data structures that are hard to update
- Ignoring caching: Refetching data unnecessarily
- 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.
