Part 6.5 — Introducing Redux Toolkit the Right Way
Redux has a reputation problem—and it earned it.
Most of that pain came from boilerplate, ceremony, and misuse, not from the core idea.
Redux Toolkit (RTK) fixes the how, not the why.
This post explains when Redux is actually justified, and how to adopt Redux Toolkit without recreating old mistakes.
1. When Redux Toolkit Is the Right Tool
You should consider Redux Toolkit when:
state spans many routes and features
multiple teams touch the same state
debugging state transitions matters
time-travel debugging is valuable
Context + reducers are getting unwieldy
Examples:
authenticated user + permissions
complex UI workflows (wizards, editors)
app-wide notifications
cached server data shared across screens
If your app is small or feature-scoped, Context may still be better.
2. What Redux Toolkit Actually Gives You
Redux Toolkit provides:
configureStore(sane defaults)createSlice(actions + reducer together)built-in Immer for immutable updates
DevTools integration by default
great TypeScript inference
It removes 90% of classic Redux boilerplate.
3. Create the Store (Minimal, Explicit)
Install dependencies:
pnpm add @reduxjs/toolkit react-reduxCreate store.ts:
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;Start empty. Add slices deliberately.
4. Define Your First Slice (Auth Example)
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type AuthState = {
user: User | null;
};
const initialState: AuthState = {
user: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
loginSuccess(state, action: PayloadAction<User>) {
state.user = action.payload;
},
logout(state) {
state.user = null;
},
},
});
export const { loginSuccess, logout } = authSlice.actions;
export default authSlice.reducer;Key points:
reducers look “mutable” (Immer handles immutability)
actions and reducer live together
intent is obvious
5. Register the Slice in the Store
import authReducer from './authSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
},
});Now your state shape is explicit:
state.auth.userNo magic.
6. Provide the Store to React
import { Provider } from 'react-redux';
<Provider store={store}>
<App />
</Provider>This mirrors Context—but with stronger tooling.
7. Typed Hooks (Non-Negotiable)
Create hooks.ts:
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;Never use untyped useDispatch or useSelector.
8. Using Redux in Components
Read state:
const user = useAppSelector(state => state.auth.user);Dispatch actions:
const dispatch = useAppDispatch();
dispatch(logout());Components stay declarative and readable.
9. Async Logic with Thunks (Keep It Boring)
Redux Toolkit supports async logic via thunks.
export const login = (email: string, password: string) =>
async (dispatch: AppDispatch) => {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await res.json();
dispatch(loginSuccess(data.user));
};Rules:
async logic lives outside reducers
reducers remain pure
side effects are explicit
RTK also supports createAsyncThunk—use it when async flows grow complex.
10. What Not to Put in Redux
Do not put:
form inputs
temporary UI state
modal open flags
component-specific state
Redux is for shared, long-lived, high-value state.
11. Redux vs Context: Clear Boundaries
Concern | Context | Redux |
|---|---|---|
Small app | ✅ | ❌ |
Feature-scoped | ✅ | ❌ |
App-wide state | ⚠️ | ✅ |
Debugging | ❌ | ✅ |
Large teams | ❌ | ✅ |
Time travel | ❌ | ✅ |
Choose based on scale, not habit.
12. Testing Redux Logic
Slices are easy to test:
it('logs user out', () => {
const state = authReducer(
{ user: { id: 1 } },
logout(),
);
expect(state.user).toBeNull();
});No React. No DOM. Pure logic.
13. Common Redux Toolkit Mistakes
Avoid:
one giant slice
deeply nested state
putting server state everywhere
dispatching from random utilities
skipping typed hooks
Redux should feel structured, not sprawling.
14. Summary
You now know:
when Redux Toolkit is justified
how it differs from Context + reducers
how to set it up cleanly
how to write slices and actions
how to integrate async logic safely
what not to store in Redux
In Part 6.6, you’ll close the loop by separating UI state from server state, handling async flows correctly, and integrating REST APIs without mixing concerns.
Related
Part 6.6 — Async State, Side Effects & Server State Boundaries
Most React state bugs come from mixing UI state with server state. This post teaches you how to draw a hard line between them—and why that line matters.
Part 6.4 — Combining Context + useReducer for App-Level State
Context provides access. Reducers provide structure. Together, they form a powerful, lightweight architecture for managing app-level state in React.
Part 6.3 — useReducer for Complex State Transitions
When state stops being simple values and starts behaving like a system, useReducer gives you structure, predictability, and clarity.
Comments