Redux — State Management
As React applications grow in complexity, managing state across many components becomes increasingly difficult. Context API works for simpler cases, but when the application has frequent state updates, deeply nested logic, or complex data flows, a dedicated state management tool becomes valuable.
Redux is the most widely adopted state management library for React. It centralizes the application's state in a single store — a global object accessible by any component — and enforces a strict, predictable pattern for updating that state.
Core Principles of Redux
- Single source of truth — The entire application state is stored in one central store.
- State is read-only — State can only be changed by dispatching actions. Components never modify state directly.
- Changes are made with pure functions — Reducers are pure functions that take the current state and an action, and return the new state.
Key Concepts
- Store — The single object that holds the entire application state.
- Action — A plain JavaScript object describing what happened. It has a
typeproperty and optional data. - Reducer — A function that takes the current state and an action, and returns the new state.
- Dispatch — The method used to send actions to the store.
- Selector — A function that reads a specific piece of data from the store.
Modern Redux — Redux Toolkit
Writing Redux manually involves significant boilerplate code. Redux Toolkit (RTK) is the official, modern way to use Redux. It dramatically simplifies the setup and eliminates most boilerplate. This topic uses Redux Toolkit throughout.
npm install @reduxjs/toolkit react-redux
Step 1 — Create a Slice
A slice is a portion of the Redux store dedicated to a specific feature. It combines the initial state, reducer functions, and action creators in one place using createSlice:
// src/store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Redux Toolkit allows this direct mutation (uses Immer internally)
},
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;
Redux Toolkit uses a library called Immer internally, which allows state to be "mutated" directly inside reducers while still producing immutable updates behind the scenes. This makes reducers much simpler to write.
Step 2 — Create the Store
// src/store/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
// Other reducers can be added here as the app grows
},
});
export default store;
Step 3 — Provide the Store to the App
Wrap the application in the Provider component from react-redux, passing the store as a prop. This makes the store available to all components:
// src/main.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(
<Provider store={store}>
<App />
</Provider>
);
Step 4 — Read and Update State in Components
Two Hooks from react-redux connect components to the Redux store:
- useSelector — Reads data from the store
- useDispatch — Sends actions to the store
// src/components/Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset } from '../store/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
export default Counter;
useSelector reads the counter.value from the global store. useDispatch provides the dispatch function to send action creators (increment(), decrement(), etc.) to the store.
Async Operations — Redux Thunk
For asynchronous operations like API calls, Redux Toolkit includes createAsyncThunk:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
return response.json();
});
const usersSlice = createSlice({
name: 'users',
initialState: { list: [], loading: false, error: null },
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.list = action.payload;
})
.addCase(fetchUsers.rejected, (state) => {
state.loading = false;
state.error = "Failed to load users";
});
},
});
export default usersSlice.reducer;
When to Use Redux
Redux is powerful but adds complexity. It is appropriate when:
- Many components across different parts of the app need access to the same state
- State logic involves complex transformations or multiple related operations
- The app needs a reliable way to debug and trace state changes over time
For smaller applications, the Context API with useReducer is often sufficient.
Key Points
- Redux centralizes all application state in a single store.
- Redux Toolkit is the modern, recommended way to use Redux — it eliminates boilerplate.
createSlicecombines actions, reducers, and initial state in one place.useSelectorreads state from the store;useDispatchsends actions to the store.- Wrap the app in
<Provider store={store}>to give all components access to the store. - Use
createAsyncThunkfor API calls and other asynchronous operations.
The next topic introduces React with TypeScript — how adding static types to React components improves code quality, catches bugs early, and improves developer experience.
