Redux
Predictable state container with actions, reducers, and a single store
Redux - Predictable State Container
Redux is a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments, and are easy to test. Redux is based on three core principles: single source of truth, state is read-only, and changes are made with pure functions.
Core Concepts
- Store — Single object holding all application state
- Actions — Plain objects describing what happened
- Reducers — Pure functions that specify how state changes
- Dispatch — Method to send actions to the store
- Selectors — Functions to extract data from state
The Redux Flow
UI Event → dispatch(action) → Reducer → New State → UI Update
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ UI │────▶│ Action │────▶│ Reducer │────▶│ Store │
│ (View) │ │ Creator │ │ │ │ (State) │
└─────────┘ └─────────┘ └─────────┘ └────┬────┘
▲ │
└───────────────────────────────────────────────┘
Subscribe & Re-render
Actions
// Actions are plain objects with a type property
const addTodo = {
type: 'todos/add',
payload: {
id: 1,
text: 'Learn Redux',
completed: false
}
};
// Action creators - functions that return actions
function addTodo(text) {
return {
type: 'todos/add',
payload: {
id: Date.now(),
text,
completed: false
}
};
}
function toggleTodo(id) {
return {
type: 'todos/toggle',
payload: { id }
};
}
function deleteTodo(id) {
return {
type: 'todos/delete',
payload: { id }
};
}
// Action type constants (prevents typos)
const ADD_TODO = 'todos/add';
const TOGGLE_TODO = 'todos/toggle';
const DELETE_TODO = 'todos/delete';
Reducers
// Reducer is a pure function: (state, action) => newState
const initialState = {
todos: [],
filter: 'all'
};
function todoReducer(state = initialState, action) {
switch (action.type) {
case 'todos/add':
return {
...state,
todos: [...state.todos, action.payload]
};
case 'todos/toggle':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'todos/delete':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
case 'filter/set':
return {
...state,
filter: action.payload
};
default:
return state;
}
}
// Combine multiple reducers
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
todos: todoReducer,
user: userReducer,
settings: settingsReducer
});
// State shape will be:
// { todos: {...}, user: {...}, settings: {...} }
Store
import { createStore } from 'redux';
// Create the store with the root reducer
const store = createStore(rootReducer);
// Get current state
console.log(store.getState());
// Dispatch actions to update state
store.dispatch(addTodo('Learn Redux'));
store.dispatch(toggleTodo(1));
// Subscribe to state changes
const unsubscribe = store.subscribe(() => {
console.log('State updated:', store.getState());
});
// Later, stop listening
unsubscribe();
React-Redux Integration
import { Provider, useSelector, useDispatch } from 'react-redux';
import { createStore } from 'redux';
// 1. Create store
const store = createStore(rootReducer);
// 2. Wrap app with Provider
function App() {
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}
// 3. Use hooks to access state and dispatch
function TodoList() {
// Select data from the store
const todos = useSelector(state => state.todos);
const filter = useSelector(state => state.filter);
// Get dispatch function
const dispatch = useDispatch();
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<ul>
{filteredTodos.map(todo => (
<li
key={todo.id}
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
<button onClick={() => dispatch(deleteTodo(todo.id))}>
Delete
</button>
</li>
))}
</ul>
);
}
function AddTodo() {
const [text, setText] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
dispatch(addTodo(text));
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add todo"
/>
<button type="submit">Add</button>
</form>
);
}
Selectors
// Basic selectors
const selectTodos = state => state.todos;
const selectFilter = state => state.filter;
// Derived data selectors
const selectCompletedTodos = state =>
state.todos.filter(todo => todo.completed);
const selectActiveTodos = state =>
state.todos.filter(todo => !todo.completed);
const selectTodoCount = state => state.todos.length;
// Parameterized selector
const selectTodoById = (state, todoId) =>
state.todos.find(todo => todo.id === todoId);
// Usage in components
function TodoStats() {
const total = useSelector(selectTodoCount);
const completed = useSelector(state => selectCompletedTodos(state).length);
const active = useSelector(state => selectActiveTodos(state).length);
return (
<div>
Total: {total} | Active: {active} | Completed: {completed}
</div>
);
}
Redux Pros & Cons
| Pros | Cons |
|---|---|
| Predictable state changes | Lots of boilerplate code |
| Excellent DevTools | Steep learning curve |
| Time-travel debugging | Can be overkill for small apps |
| Large ecosystem & community | Verbose action/reducer setup |
💡 Key Takeaways
- • Use Redux for complex apps with lots of shared state
- • Keep reducers pure - no side effects, no mutations
- • Use selectors to encapsulate state access
- • Consider Redux Toolkit for modern Redux development
- • Install Redux DevTools browser extension for debugging