React State Management Redux
Overview
Redux is a predictable state container for managing application-wide state. This chapter will learn about Redux's core concepts, integration with React, and modern Redux Toolkit usage.
🏪 Redux Core Concepts
Basic Redux Setup
jsx
// Simulate Redux functionality
function createStore(reducer, initialState) {
let state = initialState;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
return { getState, dispatch, subscribe };
}
// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_COUNT = 'SET_COUNT';
// Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const reset = () => ({ type: RESET });
const setCount = (count) => ({ type: SET_COUNT, payload: count });
// Reducer
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
case RESET:
return { ...state, count: 0 };
case SET_COUNT:
return { ...state, count: action.payload };
default:
return state;
}
}
// React Redux connection
const ReduxContext = React.createContext();
function Provider({ store, children }) {
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
React.useEffect(() => {
const unsubscribe = store.subscribe(forceUpdate);
return unsubscribe;
}, [store]);
return (
<ReduxContext.Provider value={store}>
{children}
</ReduxContext.Provider>
);
}
function useSelector(selector) {
const store = React.useContext(ReduxContext);
return selector(store.getState());
}
function useDispatch() {
const store = React.useContext(ReduxContext);
return store.dispatch;
}
// Counter component
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Redux Counter</h2>
<div style={{ fontSize: '48px', margin: '20px 0', color: '#007bff' }}>
{count}
</div>
<div>
<button
onClick={() => dispatch(decrement())}
style={{ margin: '0 10px', padding: '10px 20px' }}
>
-1
</button>
<button
onClick={() => dispatch(increment())}
style={{ margin: '0 10px', padding: '10px 20px' }}
>
+1
</button>
<button
onClick={() => dispatch(reset())}
style={{ margin: '0 10px', padding: '10px 20px' }}
>
Reset
</button>
</div>
</div>
);
}
function BasicReduxExample() {
const store = React.useMemo(() => createStore(counterReducer), []);
return (
<Provider store={store}>
<Counter />
</Provider>
);
}Complex State Management
jsx
// Todo Actions
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
const SET_FILTER = 'SET_FILTER';
const addTodo = (text) => ({
type: ADD_TODO,
payload: { id: Date.now(), text, completed: false }
});
const toggleTodo = (id) => ({
type: TOGGLE_TODO,
payload: id
});
const deleteTodo = (id) => ({
type: DELETE_TODO,
payload: id
});
const setFilter = (filter) => ({
type: SET_FILTER,
payload: filter
});
// Todo Reducer
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, action.payload];
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case DELETE_TODO:
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
// Filter Reducer
function filterReducer(state = 'ALL', action) {
switch (action.type) {
case SET_FILTER:
return action.payload;
default:
return state;
}
}
// Root Reducer
function rootReducer(state = {}, action) {
return {
todos: todosReducer(state.todos, action),
filter: filterReducer(state.filter, action)
};
}
// Todo component
function TodoApp() {
const todos = useSelector(state => state.todos || []);
const filter = useSelector(state => state.filter || 'ALL');
const dispatch = useDispatch();
const [inputValue, setInputValue] = React.useState('');
const filteredTodos = todos.filter(todo => {
switch (filter) {
case 'ACTIVE':
return !todo.completed;
case 'COMPLETED':
return todo.completed;
default:
return true;
}
});
const handleAddTodo = (e) => {
e.preventDefault();
if (inputValue.trim()) {
dispatch(addTodo(inputValue.trim()));
setInputValue('');
}
};
const todoStats = {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
};
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Redux Todo List</h2>
{/* Add todo */}
<form onSubmit={handleAddTodo} style={{ marginBottom: '20px' }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add new todo..."
style={{
padding: '10px',
width: '70%',
border: '1px solid #ddd',
borderRadius: '4px 0 0 4px'
}}
/>
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '0 4px 4px 0'
}}
>
Add
</button>
</form>
{/* Filters */}
<div style={{ marginBottom: '20px' }}>
{['ALL', 'ACTIVE', 'COMPLETED'].map(filterType => (
<button
key={filterType}
onClick={() => dispatch(setFilter(filterType))}
style={{
margin: '0 5px',
padding: '8px 16px',
backgroundColor: filter === filterType ? '#007bff' : '#f8f9fa',
color: filter === filterType ? 'white' : '#333',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
{filterType === 'ALL' ? 'All' : filterType === 'ACTIVE' ? 'Active' : 'Completed'}
</button>
))}
</div>
{/* Statistics */}
<div style={{
backgroundColor: '#f8f9fa',
padding: '15px',
borderRadius: '8px',
marginBottom: '20px'
}}>
<div>Total: {todoStats.total} | Active: {todoStats.active} | Completed: {todoStats.completed}</div>
</div>
{/* Todo list */}
<div>
{filteredTodos.length === 0 ? (
<div style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
{filter === 'ALL' ? 'No todos yet' : 'No matching todos'}
</div>
) : (
filteredTodos.map(todo => (
<div
key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
padding: '12px',
margin: '8px 0',
backgroundColor: 'white',
border: '1px solid #ddd',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
style={{ marginRight: '12px' }}
/>
<span
style={{
flex: 1,
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#6c757d' : '#333'
}}
>
{todo.text}
</span>
<button
onClick={() => dispatch(deleteTodo(todo.id))}
style={{
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
padding: '6px 12px',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Delete
</button>
</div>
))
)}
</div>
</div>
);
}
function ComplexReduxExample() {
const store = React.useMemo(() => createStore(rootReducer), []);
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}🛠️ Redux Toolkit Modern Approach
Slice and Modern Redux
jsx
// Simulate Redux Toolkit createSlice
function createSlice({ name, initialState, reducers }) {
const actionTypes = {};
const actionCreators = {};
Object.keys(reducers).forEach(key => {
const type = `${name}/${key}`;
actionTypes[key] = type;
actionCreators[key] = (payload) => ({ type, payload });
});
const reducer = (state = initialState, action) => {
const reducerKey = Object.keys(actionTypes).find(
key => actionTypes[key] === action.type
);
if (reducerKey && reducers[reducerKey]) {
return reducers[reducerKey](state, action);
}
return state;
};
return {
name,
reducer,
actions: actionCreators
};
}
// User Slice
const userSlice = createSlice({
name: 'user',
initialState: {
profile: null,
loading: false,
error: null
},
reducers: {
setLoading: (state, action) => ({
...state,
loading: action.payload
}),
setProfile: (state, action) => ({
...state,
profile: action.payload,
loading: false,
error: null
}),
setError: (state, action) => ({
...state,
error: action.payload,
loading: false
}),
clearUser: (state) => ({
...state,
profile: null,
error: null
})
}
});
// Cart Slice
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
total: 0
},
reducers: {
addItem: (state, action) => {
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return state;
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return state;
},
updateQuantity: (state, action) => {
const item = state.items.find(item => item.id === action.payload.id);
if (item) {
item.quantity = action.payload.quantity;
if (item.quantity <= 0) {
state.items = state.items.filter(i => i.id !== item.id);
}
}
state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return state;
},
clearCart: (state) => {
state.items = [];
state.total = 0;
return state;
}
}
});
// Combine Reducer
function combineReducers(slices) {
return (state = {}, action) => {
const newState = {};
Object.keys(slices).forEach(key => {
newState[key] = slices[key].reducer(state[key], action);
});
return newState;
};
}
const rootReducerToolkit = combineReducers({
user: userSlice,
cart: cartSlice
});
// E-commerce app component
function ECommerceApp() {
const user = useSelector(state => state.user);
const cart = useSelector(state => state.cart);
const dispatch = useDispatch();
const products = [
{ id: 1, name: 'iPhone 15', price: 5999 },
{ id: 2, name: 'MacBook Pro', price: 12999 },
{ id: 3, name: 'AirPods Pro', price: 1899 }
];
const handleLogin = () => {
dispatch(userSlice.actions.setLoading(true));
setTimeout(() => {
dispatch(userSlice.actions.setProfile({
id: 1,
name: 'John',
email: 'john@example.com'
}));
}, 1000);
};
const handleLogout = () => {
dispatch(userSlice.actions.clearUser());
dispatch(cartSlice.actions.clearCart());
};
return (
<div style={{ padding: '20px' }}>
<header style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px',
paddingBottom: '20px',
borderBottom: '1px solid #ddd'
}}>
<h1>Redux Toolkit E-commerce Demo</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
{user.profile ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span>Welcome, {user.profile.name}</span>
<button onClick={handleLogout}>Logout</button>
</div>
) : (
<button onClick={handleLogin} disabled={user.loading}>
{user.loading ? 'Logging in...' : 'Login'}
</button>
)}
<div style={{
backgroundColor: '#007bff',
color: 'white',
padding: '8px 12px',
borderRadius: '20px'
}}>
Cart: {cart.items.length} items ¥{cart.total}
</div>
</div>
</header>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '30px' }}>
{/* Product list */}
<div>
<h2>Product List</h2>
<div style={{ display: 'grid', gap: '15px' }}>
{products.map(product => (
<div key={product.id} style={{
padding: '20px',
border: '1px solid #ddd',
borderRadius: '8px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<h3 style={{ margin: '0 0 5px 0' }}>{product.name}</h3>
<p style={{ margin: 0, color: '#007bff', fontSize: '18px', fontWeight: 'bold' }}>
¥{product.price}
</p>
</div>
<button
onClick={() => dispatch(cartSlice.actions.addItem(product))}
style={{
backgroundColor: '#28a745',
color: 'white',
border: 'none',
padding: '10px 20px',
borderRadius: '6px'
}}
>
Add to Cart
</button>
</div>
))}
</div>
</div>
{/* Shopping cart */}
<div>
<h2>Shopping Cart</h2>
{cart.items.length === 0 ? (
<div style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
Cart is empty
</div>
) : (
<div>
{cart.items.map(item => (
<div key={item.id} style={{
padding: '15px',
border: '1px solid #ddd',
borderRadius: '8px',
marginBottom: '10px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h4 style={{ margin: '0 0 5px 0' }}>{item.name}</h4>
<p style={{ margin: 0, color: '#666' }}>¥{item.price} x {item.quantity}</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<button
onClick={() => dispatch(cartSlice.actions.updateQuantity({
id: item.id,
quantity: item.quantity - 1
}))}
style={{ padding: '5px 10px' }}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => dispatch(cartSlice.actions.updateQuantity({
id: item.id,
quantity: item.quantity + 1
}))}
style={{ padding: '5px 10px' }}
>
+
</button>
<button
onClick={() => dispatch(cartSlice.actions.removeItem(item.id))}
style={{ color: 'red', background: 'none', border: 'none' }}
>
Delete
</button>
</div>
</div>
</div>
))}
<div style={{
padding: '15px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
textAlign: 'center',
fontSize: '18px',
fontWeight: 'bold'
}}>
Total: ¥{cart.total}
</div>
<button
onClick={() => dispatch(cartSlice.actions.clearCart())}
style={{
width: '100%',
padding: '15px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '8px',
marginTop: '10px',
fontSize: '16px'
}}
>
Clear Cart
</button>
</div>
)}
</div>
</div>
</div>
);
}
function ReduxToolkitExample() {
const store = React.useMemo(() => createStore(rootReducerToolkit), []);
return (
<Provider store={store}>
<ECommerceApp />
</Provider>
);
}📝 Chapter Summary
Through this chapter, you should have mastered:
Redux Core Concepts
- ✅ Role of Store, Action, Reducer
- ✅ Unidirectional data flow and state immutability
- ✅ React-Redux connection methods
- ✅ Modern Redux Toolkit usage
Best Practices
- State structure design: Keep state flat and normalized
- Action design: Use descriptive action types
- Reducer pure functions: Avoid side effects and state mutations
- Performance optimization: Use selector optimization and memo
- Development tools: Use Redux DevTools for debugging
Continue Learning: Next Chapter - React Build and Deployment