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