Redux Toolkit / Zustand
Redux Toolkit + redux-persist is the classic state stack for React Native: predictable updates, time-travel debugging, async via thunks/RTK Query, and easy hydration from AsyncStorage. The Toolkit removes the boilerplate that made older Redux notoriously verbose.
RTK, slices, thunks, persist, hooks
EXAMPLE
// 1) Install
// npm install @reduxjs/toolkit react-redux
// npm install redux-persist @react-native-async-storage/async-storage
import { configureStore, createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';
import { Provider, useDispatch, useSelector } from 'react-redux';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist';
import { PersistGate } from 'redux-persist/integration/react';
import React from 'react';
// 2) Slice — state + reducers + actions in one
interface AuthState { userId: string | null; token: string | null }
const initialAuth: AuthState = { userId: null, token: null };
const authSlice = createSlice({
name: 'auth',
initialState: initialAuth,
reducers: {
signedIn(state, action: { payload: { userId: string; token: string } }) {
state.userId = action.payload.userId;
state.token = action.payload.token;
},
signedOut(state) { state.userId = null; state.token = null; },
},
});
export const { signedIn, signedOut } = authSlice.actions;
// 3) Async thunk
export const signIn = createAsyncThunk('auth/signIn', async (cred: { email: string; password: string }) => {
const res = await fetch('https://api.example.com/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cred),
});
if (!res.ok) throw new Error('login failed');
return res.json() as Promise<{ userId: string; token: string }>;
});
const signInReducer = createSlice({
name: 'session',
initialState: { loading: false, error: null as string | null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(signIn.pending, (s) => { s.loading = true; s.error = null; })
.addCase(signIn.fulfilled, (s) => { s.loading = false; })
.addCase(signIn.rejected, (s, a) => { s.loading = false; s.error = a.error.message ?? 'error'; });
},
});
// 4) Combine + persist
const rootReducer = combineReducers({
auth: authSlice.reducer,
session: signInReducer.reducer,
});
const persistedReducer = persistReducer(
{
key: 'root',
version: 1,
storage: AsyncStorage,
whitelist: ['auth'], // persist only 'auth' slice
},
rootReducer,
);
export const store = configureStore({
reducer: persistedReducer,
middleware: (gDM) => gDM({
serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] },
}),
});
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// 5) Typed hooks
import { TypedUseSelectorHook } from 'react-redux';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// 6) Wire up the app
export function App() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Root />
</PersistGate>
</Provider>
);
}
// 7) Component usage
function Profile() {
const userId = useAppSelector((s) => s.auth.userId);
const dispatch = useAppDispatch();
if (!userId) return <Text>Not signed in</Text>;
return (
<View>
<Text>User: {userId}</Text>
<Button title="Sign out" onPress={() => dispatch(signedOut())} />
</View>
);
}
// 8) RTK Query — data fetching alternative (recommended for server data)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
},
}),
endpoints: (b) => ({
getTodos: b.query<Todo[], void>({ query: () => '/todos' }),
addTodo: b.mutation<Todo, Partial<Todo>>({ query: (t) => ({ url: '/todos', method: 'POST', body: t }) }),
}),
});
export const { useGetTodosQuery, useAddTodoMutation } = api;
// 9) Selectors
import { createSelector } from '@reduxjs/toolkit';
const selectAuth = (s: RootState) => s.auth;
export const selectIsSignedIn = createSelector(selectAuth, (a) => !!a.userId);
export const selectShortId = createSelector(selectAuth, (a) => a.userId?.slice(0, 8));
// 10) Migrations + versioning
const persistConfig = {
key: 'root',
version: 2,
storage: AsyncStorage,
migrate: (state: any) => {
if (state?._persist?.version === 1) {
// transform v1 -> v2 state shape
return Promise.resolve({ ...state, theme: 'system' });
}
return Promise.resolve(state);
},
};
// 11) Persistent vs ephemeral state — divide explicitly
// auth, theme, language -> persist
// in-flight requests, UI state, modal open/closed -> in-memory only
// whitelist or blacklist on persistConfig.
// 12) When NOT to use Redux
// • Mostly server state -> RTK Query, React Query, or SWR
// • Local UI state -> useState
// • Theme / locale only -> Context + a single provider
// • Small apps -> Zustand, Jotai (less ceremony)
//
// Reach for Redux when you have COMPLEX SHARED MUTABLE state
// (multi-screen forms, undo/redo, time-travel debugging).
// 13) DevTools
// On Android emulator + Reactotron / Flipper, you get redux-devtools.
// configureStore enables devtools in dev by default.
// 14) Performance
// • selectors with createSelector → memoised
// • shallowEqual via useSelector(equalityFn) for object selectors
// • avoid storing huge derived data; derive in selectors
// • split big slices to avoid re-rendering unrelated subtrees
// 15) Common bugs
// • Forgot Provider → 'could not find react-redux context'
// • Forgot whitelist → entire store persists; large + slow rehydrate
// • Migration not handled → app crashes on update due to old state shape
// • Re-creating new object in selector each render → defeats memoisation
// • Storing non-serialisable values (Date, Map) → serializableCheck warns; convert to ISO strings
// • Persisting auth token in plain AsyncStorage → consider expo-secure-store / react-native-keychain for sensitive data
// • Thunks throwing without rejected handler → unhandled promise warning
// • Tightly coupling Redux state shape to API responses — write thunks that transform
Why it matters
For React Native, Redux Toolkit + redux-persist + AsyncStorage gives you predictable state with seamless hydration. Reach for RTK Query for server data, keep selectors memoised, and whitelist only the slices you actually want persistent — the persist layer is the easiest place to break startup performance.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Redux Toolkit
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counter = createSlice({
name: 'counter',
initialState: 0,
reducers: { inc: s => s + 1 }
});
export const store = configureStore({ reducer: counter.reducer });
Try it Yourself »
Discussion
Loading…