"Which state management should I use?" is still one of the most-asked React Native questions. The answer in 2026 is simpler than the number of libraries suggests, because most apps have two kinds of state, and each has a clear best tool.
First, split your state
- Server state is data that lives on a backend: products, posts, user profiles, orders. It needs caching, refetching, pagination, and invalidation.
- Client state is data that lives only in the app: theme, onboarding progress, cart drafts, which sheet is open, the current session.
Most "Redux is too complicated" pain came from putting server state in a client store and hand-writing loading flags and cache logic. Handle server state with a dedicated tool, and your client store becomes tiny.
TanStack Query: the default for server state
npx expo install @tanstack/react-query
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { Stack } from "expo-router"
const queryClient = new QueryClient()
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
)
}
import { useQuery } from "@tanstack/react-query"
export function useProducts() {
return useQuery({
queryKey: ["products"],
queryFn: () => fetch("https://api.example.com/products").then((r) => r.json()),
staleTime: 60_000,
})
}
// In a screen
const { data, isPending, error, refetch } = useProducts()
You get caching, deduplication, background refetch, retries, pagination (useInfiniteQuery), and optimistic updates without writing them yourself. Pair it with FlashList and pull-to-refresh and you have a production feed.
Zustand: the default for client state
npx expo install zustand
import { create } from "zustand"
type CartItem = { id: string; qty: number }
type CartState = {
items: CartItem[]
add: (id: string) => void
clear: () => void
}
export const useCart = create<CartState>((set) => ({
items: [],
add: (id) =>
set((s) => {
const existing = s.items.find((i) => i.id === id)
return existing
? { items: s.items.map((i) => (i.id === id ? { ...i, qty: i.qty + 1 } : i)) }
: { items: [...s.items, { id, qty: 1 }] }
}),
clear: () => set({ items: [] }),
}))
// Subscribe to just what you need, so the component only re-renders when it changes
const count = useCart((s) => s.items.length)
const add = useCart((s) => s.add)
No provider, no reducers, and no actions file. It's about 1KB, works outside React (useCart.getState()), and persists easily with MMKV.
Redux Toolkit: structure for big teams
Modern Redux is Redux Toolkit (RTK), much less verbose than classic Redux:
import { configureStore, createSlice, type PayloadAction } from "@reduxjs/toolkit"
const sessionSlice = createSlice({
name: "session",
initialState: { userId: null as string | null },
reducers: {
signedIn: (state, action: PayloadAction<string>) => {
state.userId = action.payload
},
signedOut: (state) => {
state.userId = null
},
},
})
export const { signedIn, signedOut } = sessionSlice.actions
export const store = configureStore({ reducer: { session: sessionSlice.reducer } })
Pick RTK when you have many developers who benefit from enforced patterns, you need middleware (analytics, sync, undo), or you want RTK Query as an all-in-one server and client solution. Redux DevTools' time-travel debugging is still unmatched for complex flows.
Jotai: atomic state for fine-grained UI
Jotai models state as small atoms that components subscribe to individually:
import { atom, useAtom } from "jotai"
const filterAtom = atom<"all" | "active" | "done">("all")
const todosAtom = atom<Todo[]>([])
const visibleTodosAtom = atom((get) => {
const filter = get(filterAtom)
return get(todosAtom).filter((t) => filter === "all" || (filter === "done") === t.done)
})
const [filter, setFilter] = useAtom(filterAtom)
Pick Jotai when you have lots of interdependent, derived UI state (editors, builders, complex forms) and want React-like composition without selectors.
How to choose
- Small or medium app, most teams: TanStack Query + Zustand.
- Large team, strict conventions: Redux Toolkit + RTK Query.
- Heavy derived UI state: Jotai (+ TanStack Query for server data).
- Mostly server data, little global UI state: TanStack Query +
useStateand Context. You may not need a store at all. - Offline-first or sync-heavy: look at a local database (expo-sqlite) as your source of truth and a reactive layer on top.
Recommended Expo stack for 2026
- TanStack Query for anything fetched from an API.
- Zustand for session, settings, and UI state.
- MMKV to persist Zustand instantly (guide).
- expo-secure-store for tokens (guide).
- Expo Router params for navigation state. Don't duplicate it in a store.
Common mistakes to avoid
- Copying API data into Zustand or Redux "so it's global". Let TanStack Query own it.
- Subscribing to the entire store (
useCart()) instead of selecting a slice, which causes extra re-renders. - Storing derived values. Compute them with selectors or derived atoms.
- Putting secrets in a persisted store. Use SecureStore.
Want a head start? Our Expo templates give you a typed, production-ready starting point, and our list of the best React Native libraries for 2026 covers the rest of the toolkit.