Redux Toolkit
Redux Toolkit (RTK): the modern Redux. Slices, createAsyncThunk, RTK Query, less boilerplate.
React — Redux Toolkit
EXAMPLE
// Install: npm install @reduxjs/toolkit react-redux
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { Provider, useDispatch, useSelector } from 'react-redux';
// ===== Slice =====
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
add: (state, action) => { state.items.push(action.payload); }, // Immer makes this OK
remove: (state, action) => { state.items = state.items.filter(i => i.id !== action.payload); },
clear: () => ({ items: [] }),
},
});
export const { add, remove, clear } = cartSlice.actions;
// ===== Store =====
const store = configureStore({
reducer: { cart: cartSlice.reducer },
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// ===== App =====
function App() {
return <Provider store={store}><Cart /></Provider>;
}
function Cart() {
const items = useSelector((s: RootState) => s.cart.items);
const dispatch = useDispatch<AppDispatch>();
return (
<div>
<p>{items.length} items</p>
<button onClick={() => dispatch(add({ id: Date.now(), name: 'Widget' }))}>Add</button>
</div>
);
}
// ===== Async with createAsyncThunk =====
import { createAsyncThunk } from '@reduxjs/toolkit';
export const loadUsers = createAsyncThunk('users/load', async () => {
const r = await fetch('/api/users');
if (!r.ok) throw new Error('failed');
return r.json();
});
const usersSlice = createSlice({
name: 'users',
initialState: { items: [], loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(loadUsers.pending, (s) => { s.loading = true; })
.addCase(loadUsers.fulfilled, (s, a) => { s.loading = false; s.items = a.payload; })
.addCase(loadUsers.rejected, (s, a) => { s.loading = false; s.error = a.error.message; });
},
});
// ===== RTK Query (built-in fetch + cache) =====
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (build) => ({
listUsers: build.query<User[], void>({ query: () => 'users' }),
addUser: build.mutation<User, Partial<User>>({
query: (body) => ({ url: 'users', method: 'POST', body }),
}),
}),
});
export const { useListUsersQuery, useAddUserMutation } = api;
// In a component:
function Users() {
const { data, isLoading } = useListUsersQuery();
const [addUser] = useAddUserMutation();
// ...
}
// ===== Patterns =====
// - One slice per feature
// - Use createSlice / createAsyncThunk; no manual action types
// - RTK Query for server state (replaces fetch + cache code)
// - Typed hooks (useAppDispatch, useAppSelector)
// ===== Pitfalls =====
// - Direct mutation works in slices (Immer) but feels wrong if you're used to plain Redux
// - Over-fetching with RTK Query without tags / invalidation
// - Selecting whole state -> re-renders on every change
// - Boilerplate creeping back if you ignore RTK and write old-style Redux
Why it matters
Redux Toolkit removes 90% of the old Redux boilerplate. createSlice + createAsyncThunk + RTK Query is the modern shape. Reach for it when you need middleware, time-travel debugging, or RTK Query for server state — otherwise Zustand often wins on simplicity.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counter = createSlice({ name: 'counter', initialState: 0, reducers: { inc: s => s + 1 } });
export const store = configureStore({ reducer: counter.reducer });
Try it Yourself »
Discussion
Loading…