Redux Toolkit Best Practices for Modern React Applications
A practical guide to structuring Redux Toolkit logic, slices, selectors, and async flows for large-scale frontend applications.
Redux Toolkit Best Practices for Modern React Applications
Redux Toolkit solves the biggest pain points of classic Redux by giving you a predictable, opinionated structure with almost no boilerplate. This guide focuses on practical patterns used in serious production apps, especially those with complex client-side logic.
1. Structure the Store by Feature
Organize your app by domain, not by file type.
Good structure:
src/
features/
auth/
authSlice.ts
authSelectors.ts
authThunks.ts
cart/
cartSlice.ts
cartSelectors.ts
cartThunks.ts
store.tsEach folder owns its entire feature domain.
2. Use Slices as the Main State Unit
Slices keep actions, reducers, and state in one place.
const authSlice = createSlice({
name: "auth",
initialState: { user: null, status: "idle" },
reducers: {
logout(state) {
state.user = null
}
}
})This keeps logic predictable and self-contained.
3. Async Logic Belongs in Thunks
Do not mix async logic with components.
export const loginUser = createAsyncThunk(
"auth/loginUser",
async (credentials) => {
const response = await api.login(credentials)
return response.data
}
)Components stay clean:
dispatch(loginUser({ email, password }))4. Handle Loading and Error States Consistently
Use extraReducers to manage transition states.
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state) => {
state.status = "loading"
})
.addCase(loginUser.fulfilled, (state, action) => {
state.status = "success"
state.user = action.payload
})
.addCase(loginUser.rejected, (state) => {
state.status = "error"
})
}Every async flow gets a predictable state machine.
5. Use Selectors to Encapsulate Derived Logic
Selectors prevent duplication and improve performance.
export const selectIsLoggedIn = (state) => Boolean(state.auth.user)
export const selectUserRole = (state) => state.auth.user?.roleComponent code becomes simpler and more testable.
6. Avoid Excess Normalization
Normalize only when necessary for large collections or complex updates.
Redux Toolkit includes createEntityAdapter for this use case.
7. Use useSelector Carefully
Avoid subscribing to the entire slice.
Bad:
const auth = useSelector((state) => state.auth)Good:
const user = useSelector(selectUser)This reduces unnecessary rerenders.
8. Keep UI State Out of Redux
Examples that should not be in Redux:
- modal open states
- form field values
- dropdown visibility
- simple toggles
Use component state or Zustand for UI-level state.
9. Use Immer Mutations
Redux Toolkit uses Immer under the hood. Write updates like normal JS mutations.
state.items.push(newItem)This is safe and improves readability.
10. Combine Redux Toolkit with React Query
Use Redux for:
- client-only state
- business logic
- workflows
Use React Query for:
- server data
- caching
- synchronization
Each tool stays in its lane.
11. Add Strong TypeScript Support
Use typed dispatch and selectors.
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatchTyped hooks:
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector = useSelectorFinal Thoughts
Redux Toolkit remains one of the best solutions for complex client-side logic. When combined with clear domain boundaries, predictable async flows, and strong typing, it becomes a powerful and maintainable state architecture for large frontend applications.