Redux Toolkit - Complete Implementation Guide
A comprehensive guide to implementing Redux with Redux Toolkit in a React application with modern patterns and best practices.
Table of Contents​
Installation​
# Using npm
npm install @reduxjs/toolkit react-redux
# Using yarn
yarn add @reduxjs/toolkit react-redux
Implementation Steps​
1. Create and Configure Store​
// src/store/store.js
import { configureStore } from "@reduxjs/toolkit";
import listReducer from "./slices/listSlice";
import userReducer from "./slices/userSlice";
const store = configureStore({
reducer: {
// Single reducer
lists: listReducer,
// Multiple reducers
lists: listReducer,
user: userReducer,
},
// Optional: Configure middleware
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(customMiddleware),
});
export default store;
2. Create Redux Slice​
// src/store/slices/listSlice.js
import { createSlice, nanoid } from "@reduxjs/toolkit";
const initialState = {
lists: [
{ id: nanoid(), text: "Example item 1" },
{ id: nanoid(), text: "Example item 2" },
],
loading: false,
error: null,
};
export const listSlice = createSlice({
name: "list",
initialState,
reducers: {
// Add item
addList: (state, action) => {
state.lists.push({
id: nanoid(),
text: action.payload.text,
});
},
// Delete item
deleteList: (state, action) => {
state.lists = state.lists.filter(
(list) => list.id !== action.payload.id
);
},
// Update item
updateList: (state, action) => {
const { id, text } = action.payload;
const existingList = state.lists.find(list => list.id === id);
if (existingList) {
existingList.text = text;
}
},
},
// Optional: Handle async actions
extraReducers: (builder) => {
builder
.addCase(fetchLists.pending, (state) => {
state.loading = true;
})
.addCase(fetchLists.fulfilled, (state, action) => {
state.loading = false;
state.lists = action.payload;
})
.addCase(fetchLists.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});
// Export actions
export const { addList, deleteList, updateList } = listSlice.actions;
// Export reducer
export default listSlice.reducer;
3. Provide Store to React App​
// src/main.jsx or src/index.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "./store/store";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
4. Use Redux in Components​
// src/components/ListManager.jsx
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addList, deleteList, updateList } from "../store/slices/listSlice";
function ListManager() {
const [input, setInput] = useState("");
const dispatch = useDispatch();
// Select data from store
const { lists, loading, error } = useSelector((state) => state.lists);
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
dispatch(addList({ text: input }));
setInput("");
};
// Handle item deletion
const handleDelete = (id) => {
dispatch(deleteList({ id }));
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add new item"
/>
<button type="submit">Add</button>
</form>
<ul>
{lists.map((item) => (
<li key={item.id}>
{item.text}
<button onClick={() => handleDelete(item.id)}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
export default ListManager;
Best Practices​
-
State Structure
- Keep state normalized
- Avoid deeply nested state
- Use RTK's
createEntityAdapterfor collections
-
Actions and Reducers
- Use meaningful action names
- Keep reducers pure
- Utilize RTK's
createSlicefor reducer logic
-
Selectors
- Memoize complex selectors with
createSelector - Keep selectors close to reducers
export const selectFilteredLists = createSelector(
[(state) => state.lists.lists, (state) => state.lists.filter],
(lists, filter) => lists.filter(list => list.text.includes(filter))
); - Memoize complex selectors with
-
Performance
- Use specific selectors to prevent unnecessary re-renders
- Implement proper memoization
- Split reducers for different domains
Common Patterns​
Async Actions with createAsyncThunk​
import { createAsyncThunk } from "@reduxjs/toolkit";
export const fetchLists = createAsyncThunk(
"lists/fetchLists",
async () => {
const response = await fetch("/api/lists");
return response.json();
}
);
Custom Middleware​
const loggingMiddleware = (store) => (next) => (action) => {
console.log("Dispatching:", action);
const result = next(action);
console.log("New State:", store.getState());
return result;
};
Debugging​
- Install Redux DevTools Extension
- Enable DevTools in store configuration:
const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production',
});