Skip to content

Redux Cheat Sheet

Redux Toolkit (RTK) is the standard way to manage global state: slices define reducers and actions, configureStore wires middleware, and hooks connect React components.

How to use this Redux cheat sheet

createSlice generates action creators and Immer-powered reducers from a name, initialState, and reducers object. configureStore sets up the store with Redux DevTools and default middleware including thunk support via createAsyncThunk.

useSelector reads state with memoized selectors; useDispatch sends actions. RTK Query adds data-fetching slices with caching, invalidation tags, and generated hooks—replacing much manual thunk boilerplate.

Quick Redux example

import { configureStore, createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchTodos = createAsyncThunk('todos/fetch', async () => {
  const res = await fetch('/api/todos');
  return res.json();
});

const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], status: 'idle' },
  reducers: {
    added(state, action) { state.items.push(action.payload); },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => { state.status = 'loading'; })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'idle';
        state.items = action.payload;
      });
  },
});

export const store = configureStore({ reducer: { todos: todosSlice.reducer } });
export type RootState = ReturnType<typeof store.getState>;

createSlice & configureStore

API Example Notes
createSlice createSlice({ name, initialState, reducers }) Auto-generates actions and reducer
reducers increment(state) { state.value += 1; } Immer allows "mutating" syntax
actions export todosSlice.actions.added Action creators with type prefix
configureStore configureStore({ reducer: rootReducer }) DevTools + thunk middleware by default
combineReducers { auth: authReducer, ui: uiReducer } Pass object map to reducer key
preloadedState preloadedState: { auth: { user: null } } Hydrate store from SSR or localStorage
middleware middleware: (gDM) =&gt; gDM().concat(logger) Extend default middleware chain
RootState type ReturnType&lt;typeof store.getState&gt; Typed selector input

React Hooks

Hook Example Purpose
useSelector const user = useSelector(s =&gt; s.auth.user) Subscribe to store slice
useDispatch const dispatch = useDispatch() Dispatch actions or thunks
typed hooks useAppSelector, useAppDispatch Wrap with RootState/AppDispatch types
shallowEqual useSelector(fn, shallowEqual) Avoid re-render when object fields unchanged
dispatch action dispatch(todosSlice.actions.added(item)) Sync state update
dispatch thunk dispatch(fetchTodos()) Async side effect via createAsyncThunk
Provider &lt;Provider store={store}&gt; Make store available to React tree
batch updates React 18 auto-batches Multiple dispatches in one event tick batch re-renders

createAsyncThunk

Phase Example Behavior
Define createAsyncThunk("users/fetch", async (id) =&gt; api(id)) Generates pending/fulfilled/rejected types
pending .addCase(fetch.pending, ...) Set loading flag
fulfilled .addCase(fetch.fulfilled, (s, a) =&gt; { s.data = a.payload; }) Handle success payload
rejected .addCase(fetch.rejected, (s, a) =&gt; { s.error = a.error; }) Handle failure; check a.meta.rejectedWithValue
condition { condition: (_, { getState }) =&gt; !loading } Skip duplicate in-flight requests
rejectWithValue return rejectWithValue(err.body) Pass serializable error data to reducer
unwrap await dispatch(fetch()).unwrap() Promise that throws on rejection in components
extraReducers builder builder.addCase(...) Type-safe case handling vs switch

RTK Query & Selectors

API Example Notes
createApi createApi({ reducerPath, baseQuery, endpoints }) Define data-fetching slice
baseQuery fetchBaseQuery({ baseUrl: "/api" }) HTTP wrapper with auth headers
endpoint query getPosts: builder.query({ query: () =&gt; "posts" }) Generates useGetPostsQuery hook
endpoint mutation addPost: builder.mutation({ query: (body) =&gt; ({ url: "posts", method: "POST", body }) }) POST/PUT/DELETE hooks
tags providesTags: ["Post"], invalidatesTags: ["Post"] Cache invalidation on mutations
createSelector createSelector([selectItems], items =&gt; items.filter(...)) Memoized derived state from reselect
injectEndpoints api.injectEndpoints({ endpoints: ... }) Code-split API definitions
setupListeners setupListeners(store.dispatch) Refetch on focus/reconnect

RTK Query API slice stub

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Todo'],
  endpoints: (builder) => ({
    getTodos: builder.query<Todo[], void>({
      query: () => 'todos',
      providesTags: ['Todo'],
    }),
    addTodo: builder.mutation<Todo, Partial<Todo>>({
      query: (body) => ({ url: 'todos', method: 'POST', body }),
      invalidatesTags: ['Todo'],
    }),
  }),
});
export const { useGetTodosQuery, useAddTodoMutation } = api;

Add api.reducer and api.middleware to configureStore; wrap app with RTK Query hooks.

Memoized selector

const selectTodos = (state: RootState) => state.todos.items;
export const selectActiveTodos = createSelector(
  [selectTodos],
  (todos) => todos.filter((t) => !t.completed)
);

createSelector recomputes only when input selectors return new references.

Typed React-Redux hooks

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Export typed hooks once from store.ts and use everywhere instead of raw hooks.

Common mistakes

  • Storing derived or denormalized data in Redux instead of computing with selectors.
  • Mutating state outside Immer reducers (spreading nested objects manually everywhere).
  • Putting non-serializable values (Dates, class instances, promises) in the store.
  • Fetching in useEffect when RTK Query would handle cache, loading, and errors.

Key takeaways

  • createSlice is the default for reducers; avoid hand-written switch statements.
  • createAsyncThunk standardizes async pending/fulfilled/rejected handling.
  • RTK Query replaces most manual fetch-thunk-cache logic for server state.
  • Memoized selectors prevent unnecessary re-renders in connected components.

Pro Tip

Keep server state in RTK Query and UI/ephemeral state in slices—mixing both in one mega-slice leads to tangled reducers.