To use React Redux for global state management, create a store with Redux Toolkit’s configureStore, define each piece of state and its update logic in a “slice” with createSlice, wrap your app in <Provider store={store}>, and then read state with useSelector and update it with useDispatch in any component. Redux Toolkit is the official, recommended way to write Redux today; the older createStore, hand-written action types and connect() still work but are deprecated.
This guide explains the Redux lifecycle (how data flows through a Redux app), then builds a counter and a todo list with async data loading using Redux Toolkit 2.13, React Redux 9.3 and React 19.3. I ran every example: the lifecycle logs in Node.js and the React app in a headless browser.
What Is Redux?
Redux is a predictable state container: all shared application state lives in one store, and it can only change when you dispatch an action (a plain object describing what happened) that a reducer (a pure function) turns into the next state. React Redux is the official binding that lets React components read from the store and dispatch actions. It rests on three principles:
- Single source of truth: global state is kept in one store.
- State is read-only: the only way to change it is to dispatch an action.
- Changes are made with pure functions: reducers take the previous state and an action and return the next state, without side effects.
The Redux Lifecycle (Data Flow)

Every state change in Redux follows the same one-way cycle:
- Event: the user clicks a button (or a timer fires, or data arrives).
- Dispatch: the component calls
dispatch(increment()); the action is a plain object such as{ type: 'counter/increment' }. - Middleware: the action passes through any middleware (logging, async thunks) on its way to the reducer.
- Reducer: the root reducer calls each slice reducer with its current state and the action, and they return the new state. The old state object is never modified.
- Store updated: the store saves the new state and notifies its subscribers.
- Re-render: components whose
useSelectorresult changed re-render with the new values.
To see the order for real, I added a small logging middleware and a subscriber:
// A tiny middleware that prints each step of the Redux lifecycle
export const logger = (store) => (next) => (action) => {
console.log('1. dispatched:', action.type);
console.log('2. state before:', JSON.stringify(store.getState().counter));
const result = next(action); // 3. reducers run, 4. subscribers (the UI) are notified
console.log('5. back in middleware, state after:', JSON.stringify(store.getState().counter));
return result;
};1. dispatched: counter/increment
2. state before: {"value":0}
4. subscriber notified, UI reads: 1
5. back in middleware, state after: {"value":1}Note the detail: subscribers (the UI) are notified inside next(action), as soon as the reducers return, before control comes back to the middleware. The test also confirmed the immutability rule: after another increment the previous state object still held 6 while the new one held 7, and assigning to the state directly threw TypeError: Cannot assign to read only property 'value', because Redux Toolkit freezes state in development.
When Should You Use Redux?
| Situation | Good fit |
|---|---|
| A few values shared by several components (theme, user) | React Context; see Context API for global state |
| Lots of client state updated from many places, many developers, need for strict patterns and dev tools | Redux Toolkit |
| Small to medium global state with minimal boilerplate | Zustand or Jotai |
| Data from an API (caching, refetching) | TanStack Query or RTK Query, not hand-written reducers; see fetching data with React hooks |
Redux’s strengths are predictability (every change is an action you can log and replay), the excellent Redux DevTools with time-travel debugging, and a clear structure for large teams. For React Native, see my guide to Redux Toolkit with React Native.
Step 1: Install Redux Toolkit and React Redux
npm install @reduxjs/toolkit react-reduxYou don’t need the separate redux package; Redux Toolkit includes it.
Step 2: Create a Slice
A slice holds the initial state, the reducers and the action creators for one feature:
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; }, // Immer turns this "mutation" into a new state object
decrement: (state) => { state.value -= 1; },
reset: (state) => { state.value = 0; },
incrementByAmount: (state, action) => { state.value += action.payload; },
},
});
export const { increment, decrement, reset, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;The reducers look like they mutate state, but Redux Toolkit uses the Immer library to turn those “mutations” into a new immutable state object. That only works inside createSlice and createReducer; anywhere else, never mutate state.
Step 3: Configure the Store
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import todosReducer from './todosSlice';
import { logger } from './logger';
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
});configureStore combines the slice reducers, adds the thunk middleware, turns on the Redux DevTools browser extension, and in development adds checks for accidental mutations and non-serializable values. You’d normally leave out the logger; it’s here to show the lifecycle.
Step 4: Provide the Store to React
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './redux/store';
import App from './App';
createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>,
);Step 5: Read and Update State in Components
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset } from './counterSlice';
export function Counter() {
const count = useSelector((state) => state.counter.value); // re-renders only when this value changes
const dispatch = useDispatch();
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
In my test, clicking + three times showed “Count: 3”, − brought it to 2, and Reset to 0. useSelector subscribes the component only to the value it selects, so select the smallest piece you need: during five counter updates, the todo list component below rendered zero extra times because its slice didn’t change.
Step 6: Async Data with createAsyncThunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async logic lives in a thunk; it dispatches pending / fulfilled / rejected actions for you
export const fetchTodos = createAsyncThunk('todos/fetch', async () => {
const res = await fetch('/todos.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [], status: 'idle', error: null },
reducers: {
todoAdded: (state, action) => {
state.items.push({ id: Date.now(), text: action.payload, done: false });
},
todoToggled: (state, action) => {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) todo.done = !todo.done;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => { state.status = 'loading'; })
.addCase(fetchTodos.fulfilled, (state, action) => { state.status = 'succeeded'; state.items = action.payload; })
.addCase(fetchTodos.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; });
},
});
export const { todoAdded, todoToggled } = todosSlice.actions;
export default todosSlice.reducer;import { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchTodos, todoAdded, todoToggled } from './todosSlice';
export function TodoList() {
const { items, status, error } = useSelector((state) => state.todos);
const dispatch = useDispatch();
const [text, setText] = useState('');
useEffect(() => {
if (status === 'idle') dispatch(fetchTodos());
}, [status, dispatch]);
if (status === 'loading') return <p>Loading todos...</p>;
if (status === 'failed') return <p role="alert">Error: {error}</p>;
return (
<section>
<form onSubmit={(e) => { e.preventDefault(); if (text) dispatch(todoAdded(text)); setText(''); }}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="New todo" />
<button>Add</button>
</form>
<ul>
{items.map((t) => (
<li key={t.id} onClick={() => dispatch(todoToggled(t.id))} style={{ textDecoration: t.done ? 'line-through' : 'none' }}>
{t.text}
</li>
))}
</ul>
</section>
);
}The thunk dispatched todos/fetch/pending and then todos/fetch/fulfilled (visible in the lifecycle logs), the list loaded two todos from the server, adding a todo appended it, and clicking a todo toggled it. If you fetch a lot of server data, consider RTK Query (included in Redux Toolkit), which adds caching, deduplication and refetching on top of this pattern.
mapStateToProps and connect: The Older API
Many tutorials and codebases use connect() with mapStateToProps. It still works, but React Redux 9.3 marks connect as deprecated (it shows a deprecation warning in your editor; legacy_connect silences it) in favour of hooks:
// The older connect() + mapStateToProps style, for comparison
import { connect } from 'react-redux';
import { increment } from './counterSlice';
function LegacyCounter({ count, onIncrement }) {
// Wrap the call: passing onIncrement directly would send the click event as the payload
return <button onClick={() => onIncrement()}>Legacy count: {count}</button>;
}
const mapStateToProps = (state) => ({ count: state.counter.value });
const mapDispatchToProps = { onIncrement: increment };
export default connect(mapStateToProps, mapDispatchToProps)(LegacyCounter);This one has a gotcha I hit while testing. With the object form of mapDispatchToProps, writing onClick={onIncrement} passes the click event as the action’s payload, and Redux Toolkit warned: A non-serializable value was detected in an action, in the path: payload. Wrapping it as onClick={() => onIncrement()} fixed it.
| connect API | Hooks equivalent |
|---|---|
mapStateToProps = (state) => ({ count: state.counter.value }) | const count = useSelector((s) => s.counter.value) |
mapDispatchToProps = { onIncrement: increment } | const dispatch = useDispatch(); dispatch(increment()) |
export default connect(...)(Component) | Export the component directly |
Common Mistakes
- Mutating state outside
createSlicereducers, or returning a new value and mutating in the same reducer. - Putting non-serializable values (class instances, promises, DOM events, functions) in actions or state.
- Selecting large objects (
useSelector((s) => s)), which re-renders the component on every change. - Storing everything in Redux: form input and UI state usually belong in component state.
- Copying server data into reducers by hand when RTK Query or TanStack Query would handle caching for you.
Conclusion
Redux gives React apps one predictable place for global state: components dispatch actions, reducers produce the next state, the store notifies subscribers and the right components re-render. With Redux Toolkit, that’s a few lines per feature: createSlice for state and reducers, configureStore for the store, Provider at the root, and useSelector and useDispatch in components, plus createAsyncThunk or RTK Query for server data. For smaller apps, Context or Zustand may be all you need. The official Redux Essentials tutorial goes further.
FAQs
What is the Redux lifecycle?
A component dispatches an action, the action passes through middleware to the reducers, the reducers return a new state, the store saves it and notifies subscribers, and components whose selected values changed re-render.
How do I use Redux with React?
Install @reduxjs/toolkit and react-redux, create slices with createSlice, combine them with configureStore, wrap your app in Provider, and use useSelector to read state and useDispatch to dispatch actions in components.
Is createStore deprecated?
It is marked deprecated in favour of configureStore from Redux Toolkit. It still works, and legacy_createStore avoids the warning, but new code should use Redux Toolkit.
Is mapStateToProps still used?
Only in older code. React Redux 9.3 marks connect as deprecated; use the useSelector and useDispatch hooks instead. mapStateToProps maps directly to a useSelector call.
Redux vs Context API: which should I use?
Context is enough for a few rarely changing values. Redux Toolkit suits large, frequently updated state shared across many components, with strict patterns and powerful dev tools. Zustand is a lighter middle ground.
Why does my Redux state not update?
Common causes: mutating state outside a createSlice reducer, forgetting to wrap the app in Provider, dispatching the action creator itself instead of calling it (dispatch(increment) instead of dispatch(increment())), or selecting the wrong part of the state.








