Redux
02 / 02

Async Logic, Selectors & Performance

Async Logic, Selectors & Performance

Async Logic with Thunks

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

// A reducer must be a pure, synchronous function — async logic can't fit
// that contract, so it lives in a thunk instead, dispatching plain
// synchronous actions once the async work resolves
export const fetchUser = createAsyncThunk('user/fetch', async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
});

const userSlice = createSlice({
  name: 'user',
  initialState: { data: null, status: 'idle' },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => { state.status = 'loading'; })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.data = action.payload;
      });
  },
});

Memoized Selectors

import { createSelector } from '@reduxjs/toolkit';

// BAD — a new object every call means useSelector's reference-equality
// check never sees "unchanged", causing re-renders on every store update
const selectStats = state => ({
  total: state.todos.length,
  completed: state.todos.filter(t => t.completed).length,
});

// GOOD — recomputes only when state.todos actually changes, returns the
// SAME cached reference otherwise
const selectStats = createSelector(
  state => state.todos,
  (todos) => ({
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
  })
);

// Or a shallow-equality check as a lighter alternative for simple cases
import { shallowEqual } from 'react-redux';
const stats = useSelector(selectStats, shallowEqual);

Normalized State

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

// Instead of nested/duplicated data (the same author embedded in many
// posts), store each entity once, referenced by ID
const postsAdapter = createEntityAdapter();

const postsSlice = createSlice({
  name: 'posts',
  initialState: postsAdapter.getInitialState(),
  reducers: {
    postAdded: postsAdapter.addOne,
    postUpdated: postsAdapter.updateOne,
  },
});
// State shape: { ids: [...], entities: { '1': {...}, '2': {...} } }
// Updating one post touches exactly one place, regardless of how many
// other structures might otherwise have embedded a stale copy of it.

What Belongs in Redux

Reserve the global store for genuinely shared/cross-cutting state. Purely local, ephemeral UI state (is this dropdown open, is this input focused) usually belongs in component state (useState) instead — putting it in Redux adds indirection and can cause unrelated re-renders. For server data specifically, RTK Query auto-generates hooks handling fetching, caching, and invalidation, recognizing that "server state" behaves differently from genuine client-only state.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free