Master modern state management in React applications using Redux Toolkit, the official, opinionated toolset for efficient Redux development. 🚀
Project Setup 🎯
First, let's set up our project with all necessary dependencies.
# Create new React project
npx create-react-app my-app --template redux-typescript
# Or add to existing project
npm install @reduxjs/toolkit react-redux
Store Configuration 🏗️
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import authReducer from './slices/authSlice';
import todoReducer from './slices/todoSlice';
import uiReducer from './slices/uiSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
todos: todoReducer,
ui: uiReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these paths in the state
ignoredActions: ['auth/loginSuccess'],
ignoredPaths: ['auth.user']
}
})
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Create typed hooks
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Feature Slices 🍕
Authentication Slice
// src/store/slices/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { AuthService } from '../../services/auth';
interface User {
id: string;
email: string;
name: string;
}
interface AuthState {
user: User | null;
token: string | null;
loading: boolean;
error: string | null;
}
const initialState: AuthState = {
user: null,
token: localStorage.getItem('token'),
loading: false,
error: null
};
export const login = createAsyncThunk(
'auth/login',
async (credentials: { email: string; password: string }, { rejectWithValue }) => {
try {
const response = await AuthService.login(credentials);
localStorage.setItem('token', response.token);
return response;
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const logout = createAsyncThunk(
'auth/logout',
async (_, { rejectWithValue }) => {
try {
await AuthService.logout();
localStorage.removeItem('token');
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setUser: (state, action: PayloadAction<User>) => {
state.user = action.payload;
},
clearError: (state) => {
state.error = null;
}
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload.user;
state.token = action.payload.token;
})
.addCase(login.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(logout.fulfilled, (state) => {
state.user = null;
state.token = null;
});
}
});
export const { setUser, clearError } = authSlice.actions;
export default authSlice.reducer;
Todo Slice
// src/store/slices/todoSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { TodoService } from '../../services/todo';
interface Todo {
id: string;
title: string;
completed: boolean;
}
interface TodoState {
items: Todo[];
loading: boolean;
error: string | null;
}
const initialState: TodoState = {
items: [],
loading: false,
error: null
};
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async (_, { rejectWithValue }) => {
try {
return await TodoService.getAll();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const addTodo = createAsyncThunk(
'todos/addTodo',
async (title: string, { rejectWithValue }) => {
try {
return await TodoService.create({ title });
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const toggleTodo = createAsyncThunk(
'todos/toggleTodo',
async (id: string, { getState, rejectWithValue }) => {
try {
const state = getState() as { todos: TodoState };
const todo = state.todos.items.find(item => item.id === id);
if (!todo) throw new Error('Todo not found');
return await TodoService.update(id, {
completed: !todo.completed
});
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
clearTodos: (state) => {
state.items = [];
}
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(addTodo.fulfilled, (state, action) => {
state.items.push(action.payload);
})
.addCase(toggleTodo.fulfilled, (state, action) => {
const index = state.items.findIndex(
todo => todo.id === action.payload.id
);
if (index !== -1) {
state.items[index] = action.payload;
}
});
}
});
export const { clearTodos } = todoSlice.actions;
export default todoSlice.reducer;
UI Slice
// src/store/slices/uiSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface UIState {
theme: 'light' | 'dark';
sidebarOpen: boolean;
notifications: Array<{
id: string;
message: string;
type: 'success' | 'error' | 'info';
}>;
}
const initialState: UIState = {
theme: 'light',
sidebarOpen: true,
notifications: []
};
const uiSlice = createSlice({
name: 'ui',
initialState,
reducers: {
toggleTheme: (state) => {
state.theme = state.theme === 'light' ? 'dark' : 'light';
},
toggleSidebar: (state) => {
state.sidebarOpen = !state.sidebarOpen;
},
addNotification: (state, action: PayloadAction<Omit<UIState['notifications'][0], 'id'>>) => {
state.notifications.push({
...action.payload,
id: Date.now().toString()
});
},
removeNotification: (state, action: PayloadAction<string>) => {
state.notifications = state.notifications.filter(
n => n.id !== action.payload
);
}
}
});
export const {
toggleTheme,
toggleSidebar,
addNotification,
removeNotification
} = uiSlice.actions;
export default uiSlice.reducer;
Custom Hooks 🎣
// src/hooks/useAuth.ts
import { useCallback } from 'react';
import { useAppSelector, useAppDispatch } from '../store';
import { login, logout, clearError } from '../store/slices/authSlice';
export const useAuth = () => {
const dispatch = useAppDispatch();
const { user, loading, error } = useAppSelector(state => state.auth);
const handleLogin = useCallback(async (
email: string,
password: string
) => {
try {
await dispatch(login({ email, password })).unwrap();
return true;
} catch (error) {
return false;
}
}, [dispatch]);
const handleLogout = useCallback(async () => {
try {
await dispatch(logout()).unwrap();
return true;
} catch (error) {
return false;
}
}, [dispatch]);
const handleClearError = useCallback(() => {
dispatch(clearError());
}, [dispatch]);
return {
user,
loading,
error,
login: handleLogin,
logout: handleLogout,
clearError: handleClearError
};
};
// src/hooks/useTodos.ts
import { useCallback } from 'react';
import { useAppSelector, useAppDispatch } from '../store';
import {
fetchTodos,
addTodo,
toggleTodo,
clearTodos
} from '../store/slices/todoSlice';
export const useTodos = () => {
const dispatch = useAppDispatch();
const { items, loading, error } = useAppSelector(state => state.todos);
const handleFetch = useCallback(async () => {
try {
await dispatch(fetchTodos()).unwrap();
return true;
} catch (error) {
return false;
}
}, [dispatch]);
const handleAdd = useCallback(async (
title: string
) => {
try {
await dispatch(addTodo(title)).unwrap();
return true;
} catch (error) {
return false;
}
}, [dispatch]);
const handleToggle = useCallback(async (
id: string
) => {
try {
await dispatch(toggleTodo(id)).unwrap();
return true;
} catch (error) {
return false;
}
}, [dispatch]);
const handleClear = useCallback(() => {
dispatch(clearTodos());
}, [dispatch]);
return {
todos: items,
loading,
error,
fetchTodos: handleFetch,
addTodo: handleAdd,
toggleTodo: handleToggle,
clearTodos: handleClear
};
};
Components Integration 🧩
// src/components/TodoList.tsx
import React, { useEffect } from 'react';
import { useTodos } from '../hooks/useTodos';
export const TodoList: React.FC = () => {
const {
todos,
loading,
error,
fetchTodos,
addTodo,
toggleTodo
} = useTodos();
useEffect(() => {
fetchTodos();
}, [fetchTodos]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>Todo List</h2>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer'
}}
>
{todo.title}
</li>
))}
</ul>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const input = form.elements.namedItem('title') as HTMLInputElement;
addTodo(input.value);
form.reset();
}}
>
<input
name="title"
type="text"
placeholder="Add new todo"
required
/>
<button type="submit">Add</button>
</form>
</div>
);
};
// src/components/AuthForm.tsx
import React from 'react';
import { useAuth } from '../hooks/useAuth';
export const AuthForm: React.FC = () => {
const { loading, error, login, clearError } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const email = (form.elements.namedItem('email') as HTMLInputElement).value;
const password = (form.elements.namedItem('password') as HTMLInputElement).value;
await login(email, password);
};
return (
<form onSubmit={handleSubmit}>
{error && (
<div onClick={clearError}>
Error: {error}
</div>
)}
<input
name="email"
type="email"
placeholder="Email"
required
/>
<input
name="password"
type="password"
placeholder="Password"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Loading...' : 'Login'}
</button>
</form>
);
};
Performance Optimization 🚀
// src/store/selectors.ts
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from './index';
// Memoized selectors
export const selectCompletedTodos = createSelector(
(state: RootState) => state.todos.items,
(todos) => todos.filter(todo => todo.completed)
);
export const selectTodoStats = createSelector(
(state: RootState) => state.todos.items,
(todos) => ({
total: todos.length,
completed: todos.filter(todo => todo.completed).length,
incomplete: todos.filter(todo => !todo.completed).length
})
);
// Usage in component
const TodoStats: React.FC = () => {
const stats = useAppSelector(selectTodoStats);
return (
<div>
<p>Total: {stats.total}</p>
<p>Completed: {stats.completed}</p>
<p>Incomplete: {stats.incomplete}</p>
</div>
);
};
Best Practices 📝
- Use TypeScript for type safety
- Implement proper error handling
- Use createAsyncThunk for async operations
- Implement proper selectors
- Use proper state normalization
- Follow Redux Toolkit conventions
- Implement proper testing
- Use proper documentation
- Follow immutability patterns
- Optimize performance with memoization
Additional Resources
Redux Toolkit simplifies Redux development by providing utilities to handle common use cases, reducing boilerplate, and enforcing best practices. This guide provides a solid foundation for building scalable applications with Redux Toolkit.