React State Management with Redux Toolkit: Simplified
React State Management with Redux Toolkit: Simplified
Redux Toolkit is the official recommended way to write Redux logic. It simplifies Redux development by reducing boilerplate and providing good defaults.
Why Redux Toolkit?
Traditional Redux requires:
- Action type constants
- Action creators
- Switch statements in reducers
- Manual immutable updates
- Separate files for each piece
Redux Toolkit handles all of this with createSlice.
Installation
npm install @reduxjs/toolkit react-redux
Basic Setup
Create a Store
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
Create a Slice
// features/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
status: 'idle',
},
reducers: {
increment: (state) => {
state.value += 1; // Redux Toolkit allows "mutations"!
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
},
},
});
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;
Provider Setup
// index.js
import { Provider } from 'react-redux';
import store from './store';
root.render(
<Provider store={store}>
<App />
</Provider>
);
Using in Components
useSelector and useDispatch
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './features/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(5))}>
Add 5
</button>
</div>
);
}
Typed Hooks (TypeScript)
// 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;
// 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;
Async Actions with createAsyncThunk
// features/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch');
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const userSlice = createSlice({
name: 'users',
initialState: {
entities: [],
loading: 'idle',
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.loading = 'pending';
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.loading = 'idle';
state.entities.push(action.payload);
})
.addCase(fetchUserById.rejected, (state, action) => {
state.loading = 'idle';
state.error = action.payload;
});
},
});
Real-World Example: Todo App
// features/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchTodos = createAsyncThunk('todos/fetchAll', async () => {
const response = await fetch('/api/todos');
return response.json();
});
export const addTodo = createAsyncThunk('todos/add', async (title) => {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, completed: false }),
});
return response.json();
});
export const toggleTodo = createAsyncThunk('todos/toggle', async (todo) => {
const response = await fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: !todo.completed }),
});
return response.json();
});
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
status: 'idle',
error: null,
},
reducers: {
clearCompleted: (state) => {
state.items = state.items.filter((todo) => !todo.completed);
},
},
extraReducers: (builder) => {
builder
// Fetch todos
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
// Add todo
.addCase(addTodo.fulfilled, (state, action) => {
state.items.push(action.payload);
})
// Toggle todo
.addCase(toggleTodo.fulfilled, (state, action) => {
const index = state.items.findIndex((t) => t.id === action.payload.id);
if (index !== -1) {
state.items[index] = action.payload;
}
});
},
});
export const { clearCompleted } = todosSlice.actions;
export default todosSlice.reducer;
Using the Todo Slice
function TodoList() {
const dispatch = useAppDispatch();
const { items, status, error } = useAppSelector((state) => state.todos);
const [newTodo, setNewTodo] = useState('');
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
const handleAdd = () => {
if (newTodo.trim()) {
dispatch(addTodo(newTodo));
setNewTodo('');
}
};
if (status === 'loading') return <div>Loading...</div>;
if (status === 'failed') return <div>Error: {error}</div>;
return (
<div>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add todo"
/>
<button onClick={handleAdd}>Add</button>
<ul>
{items.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo))}
/>
{todo.title}
</li>
))}
</ul>
<button onClick={() => dispatch(clearCompleted())}>
Clear Completed
</button>
</div>
);
}
Best Practices
1. Normalize Data
import { createEntityAdapter } from '@reduxjs/toolkit';
const todosAdapter = createEntityAdapter({
sortComparer: (a, b) => a.title.localeCompare(b.title),
});
const todosSlice = createSlice({
name: 'todos',
initialState: todosAdapter.getInitialState({
status: 'idle',
}),
reducers: {
addTodo: todosAdapter.addOne,
updateTodo: todosAdapter.updateOne,
deleteTodo: todosAdapter.removeOne,
},
});
// Selectors
export const {
selectAll: selectAllTodos,
selectById: selectTodoById,
selectIds: selectTodoIds,
} = todosAdapter.getSelectors((state) => state.todos);
2. Use createApi for Data Fetching
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Todos'],
endpoints: (builder) => ({
getTodos: builder.query({
query: () => '/todos',
providesTags: ['Todos'],
}),
addTodo: builder.mutation({
query: (todo) => ({
url: '/todos',
method: 'POST',
body: todo,
}),
invalidatesTags: ['Todos'],
}),
}),
});
export const { useGetTodosQuery, useAddTodoMutation } = api;
3. Organize Store Structure
src/
├── store.js
├── hooks.js
├── features/
│ ├── counter/
│ │ └── counterSlice.js
│ ├── todos/
│ │ └── todosSlice.js
│ └── users/
│ └── usersSlice.js
└── api/
└── apiSlice.js
Performance Tips
// Use memoized selectors
import { createSelector } from '@reduxjs/toolkit';
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'completed':
return todos.filter((t) => t.completed);
case 'active':
return todos.filter((t) => !t.completed);
default:
return todos;
}
}
);
Conclusion
Redux Toolkit dramatically simplifies Redux development by:
- Eliminating boilerplate
- Providing good defaults
- Enabling "mutable" syntax
- Including RTK Query for data fetching
It's the recommended way to use Redux in modern React applications!