Concept
This topic doesn't introduce new mechanics, it synthesizes the rules established across Store, Actions & Reducers, Middleware, Selectors & Reselect, and Data Normalization into a single set of priorities, following the official Redux style guide's own tiering: Essential (non-negotiable, breaks things if violated), Strongly Recommended (skip only with a specific reason), and Recommended (good defaults, more context-dependent).
Essential: rules that break things if violated
Never mutate state directly. Every reducer-purity requirement from Actions & Reducers traces back to this, mutation breaks reference-equality checks that useSelector, createSelector, and Redux DevTools' time-travel all depend on.
// ❌ ESSENTIAL violation
function reducer(state, action) {
state.items.push(action.payload); // mutates, then returns the SAME reference
return state;
}Reducers must have zero side effects. No API calls, no Math.random(), no Date.now() inside a reducer, a reducer must be a pure, deterministic function of (state, action). Any actual side effect belongs in middleware (covered in Middleware), not the reducer.
Don't put non-serializable values in state or actions. Functions, Promises, class instances, Map/Set, none of these survive serialization, which breaks Redux DevTools (can't log/replay them) and persistence (redux-persist-style tools can't serialize them to storage).
Strongly recommended: skip only with a specific reason
Use Redux Toolkit, not hand-written createStore/combineReducers. Every mechanic covered in this redux-core domain (immutability, action creators, combineReducers) still applies under the hood, RTK's configureStore/createSlice (covered in configureStore and createSlice) just eliminates the boilerplate and hazards (like accidental mutation) around writing it by hand, via Immer.
Normalize relational/nested state, covered in full in Data Normalization, this isn't a niche optimization, it's the default shape for any state representing multiple related entities.
Structure state shape around what the UI actually needs, not around whatever shape the API happens to return, normalized { ids, entities } per resource type, not a 1:1 mirror of API response bodies.
Recommended: good defaults, more context-dependent
Keep reducers focused on a single slice. combineReducers (from Actions & Reducers) exists specifically so each slice reducer only needs to reason about its own piece of state.
Use selectors, not direct state.x.y.z access, throughout components. Confirmed in Selectors & Reselect: this centralizes the "how is this data shaped/derived" logic in one place, and makes later shape changes (e.g. normalizing something that wasn't before) a one-file change instead of a find-and-replace across every consuming component.
A worked example: applying the priority order to a real decision
Suppose a reducer needs to add a timestamp to every logged action. The Essential-tier rule ("reducers must have zero side effects, no Date.now()") rules out calling Date.now() inside the reducer directly:
// ❌ violates the Essential "no side effects in reducers" rule
function reducer(state, action) {
if (action.type === "log/entry") {
return { ...state, entries: [...state.entries, { text: action.payload, time: Date.now() }] };
}
return state;
}The correct fix respects the tier: compute the timestamp before dispatch (in the action creator, or in middleware, see Middleware) and pass it as part of the action's payload, keeping the reducer itself a pure function of its two inputs:
function logEntry(text) {
return { type: "log/entry", payload: { text, time: Date.now() } }; // side effect lives OUTSIDE the reducer
}