Skip to content
FLAVIO COPES
flaviocopes.com
2026

Learn how to use Redux

By

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:

  1. The store holds the current state.
  2. A component dispatches an action describing what happened.
  3. A reducer calculates the next state.
  4. 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.

Tagged: React · All topics
~~~

Related posts about react: