Scientyfic World

How to Use React Redux for Global State: Redux Toolkit Guide and the Redux Lifecycle

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...

Share:

Get an AI summary of this article

React Redux for Global State Management blog banner

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:

  1. Single source of truth: global state is kept in one store.
  2. State is read-only: the only way to change it is to dispatch an action.
  3. 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)

Redux lifecycle diagram: the user triggers an action, it is dispatched to the reducer, the reducer creates the new state in the store, and components re-render
The Redux lifecycle. The exact order of each step is listed below.

Every state change in Redux follows the same one-way cycle:

  1. Event: the user clicks a button (or a timer fires, or data arrives).
  2. Dispatch: the component calls dispatch(increment()); the action is a plain object such as { type: 'counter/increment' }.
  3. Middleware: the action passes through any middleware (logging, async thunks) on its way to the reducer.
  4. 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.
  5. Store updated: the store saves the new state and notifies its subscribers.
  6. Re-render: components whose useSelector result 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?

SituationGood 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 toolsRedux Toolkit
Small to medium global state with minimal boilerplateZustand 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-redux

You 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>
  );
}
React Redux counter demo with +, - and Reset buttons
The counter in action (recorded with the original version of this demo; the Redux Toolkit version below behaves the same).

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 APIHooks 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 createSlice reducers, 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.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts