Learn how to use Redux
By Flavio Copes
Learn the current Redux approach with Redux Toolkit: create a slice, configure the store, connect React, and read or update shared state.
Redux is a library for managing shared application state. Today, the official way to write Redux code is Redux Toolkit.
You might use Redux when state is shared by many parts of an application, changes in several ways, and has become hard to manage with local component state alone.
Do not add Redux automatically to every React application. React state and context are often enough for smaller applications.
The Redux data flow
Redux has a one-way data flow:
- The store holds the current state.
- A component dispatches an action describing what happened.
- A reducer calculates the next state.
- Subscribed components render with the new state.
Redux Toolkit keeps these ideas but removes most of the boilerplate that older Redux examples required.
Install Redux Toolkit
For a React application, install Redux Toolkit and React Redux:
npm install @reduxjs/toolkit react-redux
@reduxjs/toolkit contains the recommended Redux APIs. react-redux connects the store to React.
Create a slice
A slice groups the state, reducer logic, and generated action creators for one feature.
Create features/todos/todosSlice.js:
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
items: [],
}
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
state.items.push({
id: crypto.randomUUID(),
text: action.payload,
completed: false,
})
},
todoToggled(state, action) {
const todo = state.items.find((item) => item.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
},
todoRemoved(state, action) {
state.items = state.items.filter((item) => item.id !== action.payload)
},
},
})
export const { todoAdded, todoToggled, todoRemoved } = todosSlice.actions
export default todosSlice.reducer
The reducer code appears to mutate state. This is safe inside createSlice: Redux Toolkit uses Immer to produce an immutable next state from those changes.
Do not mutate Redux state outside a Redux Toolkit reducer.
Configure the store
Create app/store.js:
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from '../features/todos/todosSlice.js'
export const store = configureStore({
reducer: {
todos: todosReducer,
},
})
configureStore() combines the reducers, adds useful development checks, and enables the Redux DevTools Extension.
The resulting state has this shape:
{
todos: {
items: []
}
}
Make the store available to React
Wrap the application with React Redux’s Provider:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App.jsx'
import { store } from './app/store.js'
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
)
Components inside Provider can now read from the store and dispatch actions.
Read state with useSelector
useSelector() receives the entire Redux state and returns the part a component needs:
import { useSelector } from 'react-redux'
export default function TodoList() {
const todos = useSelector((state) => state.todos.items)
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
The component renders again when the selected value changes.
Selectors can be moved into the slice file when you reuse them:
export const selectTodos = (state) => state.todos.items
Then use useSelector(selectTodos).
Dispatch actions with useDispatch
useDispatch() returns the store’s dispatch function:
import { useState } from 'react'
import { useDispatch } from 'react-redux'
import { todoAdded } from './features/todos/todosSlice.js'
export default function AddTodo() {
const [text, setText] = useState('')
const dispatch = useDispatch()
function handleSubmit(event) {
event.preventDefault()
const value = text.trim()
if (!value) return
dispatch(todoAdded(value))
setText('')
}
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(event) => setText(event.target.value)}
/>
<button>Add todo</button>
</form>
)
}
todoAdded(value) creates an action such as:
{
type: 'todos/todoAdded',
payload: 'Buy milk'
}
The store sends that action to the slice reducer, which calculates the next state.
Load asynchronous data
Reducers must be synchronous and must not perform side effects. Put network requests outside reducers.
Redux Toolkit includes createAsyncThunk() for request lifecycles, and RTK Query for fetching and caching server data.
Before adding a thunk, consider whether RTK Query or your framework’s data-loading API already solves the problem.
What happened to createStore?
Older tutorials use createStore(), hand-written action type constants, combineReducers(), and large switch statements.
Those APIs still explain Redux’s foundations, but the Redux maintainers recommend Redux Toolkit for new code. configureStore() replaces the typical createStore() setup, and createSlice() generates action creators and reducer cases together.
The official Redux Toolkit quick start and Why Redux Toolkit is Redux Today explain the current approach in more detail.
Related posts about react: