iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…