Store

Introduction

Redux Toolkit is the standard way to write Redux. This reference targets Redux Toolkit 2.x and React-Redux 9.x, covering slices, thunks, RTK Query, and hooks.

Install

npm install @reduxjs/toolkit react-redux

@reduxjs/toolkit bundles Redux, Immer, and Reselect; react-redux provides the bindings.

See: Getting started

Creating a store

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})

configureStore combines slice reducers, adds thunk middleware, and enables DevTools and development checks.

See: configureStore

Middleware

import { configureStore } from '@reduxjs/toolkit'
import { api } from './api'

export const store = configureStore({
  reducer: { [api.reducerPath]: api.reducer },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware)
})

getDefaultMiddleware() keeps the defaults typed; use .concat() or .prepend() rather than spreading.

See: configureStore

Slices

Defining a slice

import { createSlice, type PayloadAction } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    incremented: (state) => {
      state.value += 1
    },
    added: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    }
  }
})

export const { incremented, added } = counterSlice.actions
export default counterSlice.reducer

createSlice generates the action creators and types. Reducers may mutate state; Immer keeps it immutable.

See: createSlice

Async thunks

import { createAsyncThunk } from '@reduxjs/toolkit'

export const fetchUser = createAsyncThunk(
  'users/fetch',
  async (id: number, { rejectWithValue }) => {
    const res = await fetch(`/api/users/${id}`)
    if (!res.ok) return rejectWithValue(await res.text())
    return (await res.json()) as User
  }
)

The thunk dispatches pending, fulfilled, and rejected actions; rejectWithValue() sets the rejected payload.

See: createAsyncThunk

Handling thunk results

extraReducers: (builder) => {
  builder
    .addCase(fetchUser.pending, (state) => {
      state.status = 'loading'
    })
    .addCase(fetchUser.fulfilled, (state, action) => {
      state.user = action.payload
      state.status = 'idle'
    })
    .addCase(fetchUser.rejected, (state, action) => {
      state.error = action.payload as string
    })
}

Handle lifecycle actions in extraReducers, one builder.addCase() per action creator.

See: createAsyncThunk

Selectors

import { createSelector } from '@reduxjs/toolkit'
import type { RootState } from './store'

export const selectItems = (state: RootState) => state.cart.items

export const selectTotal = createSelector([selectItems], (items) =>
  items.reduce((total, item) => total + item.price, 0)
)

createSelector memoizes derived values, so components skip re-rendering when inputs are unchanged.

See: createSelector

RTK Query

API slice

import {
  createApi,
  fetchBaseQuery
} from '@reduxjs/toolkit/query/react'

type UserPatch = Partial<User> & { id: number }

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['User'],
  endpoints: (build) => ({
    getUser: build.query<User, number>({
      query: (id) => `users/${id}`,
      providesTags: ['User']
    }),
    updateUser: build.mutation<User, UserPatch>({
      query: ({ id, ...patch }) => ({
        url: `users/${id}`,
        method: 'PATCH',
        body: patch
      }),
      invalidatesTags: ['User']
    })
  })
})

export const { useGetUserQuery, useUpdateUserMutation } = api

One API slice per base URL. tagTypes powers cache invalidation after mutations.

See: RTK Query overview

Adding to the store

import { configureStore } from '@reduxjs/toolkit'
import { api } from './api'

export const store = configureStore({
  reducer: { [api.reducerPath]: api.reducer },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware)
})

Both the reducer and the middleware are required; the middleware drives caching, invalidation, and polling.

See: RTK Query overview

Query hooks

import { useGetUserQuery } from './api'

function User ({ id }: { id: number }) {
  const { data, isLoading, error } = useGetUserQuery(id)
  if (isLoading) return <p>Loading…</p>
  if (error) return <p>Failed</p>
  return <p>{data?.name}</p>
}

Hooks fetch on mount, refetch when the argument changes, and share one cache entry per argument.

See: RTK Query overview

React

Provider

import { Provider } from 'react-redux'
import { createRoot } from 'react-dom/client'
import { store } from './store'

createRoot(document.getElementById('root')!).render(
  <Provider store={store}>
    <App />
  </Provider>
)

<Provider> supplies the store to useSelector and useDispatch. Wrap the app once, at the root.

See: Provider

Typed hooks

import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()

withTypes is a no-op at runtime; it types both hooks for the whole app.

See: Usage with TypeScript

Reading state

import { useAppDispatch, useAppSelector } from './store'
import { incremented } from './counterSlice'

function Counter () {
  const value = useAppSelector((state) => state.counter.value)
  const dispatch = useAppDispatch()
  return (
    <button onClick={() => dispatch(incremented())}>{value}</button>
  )
}

Select the smallest value you need; useSelector re-renders when the returned reference changes.

See: Hooks

Legacy Redux

createStore

import { legacy_createStore as createStore } from 'redux'

const store = createStore(counter, { value: 0 })

createStore is deprecated: it renders with a strikethrough but still works. Use configureStore in new code.

See: createStore

connect

import { connect } from 'react-redux'

export default connect(
  (state) => ({ message: state.message }),
  (dispatch) => ({
    onMessageClick: (message) => dispatch({ type: 'click', message })
  })
)(App)

connect is deprecated as of React-Redux 9.3.0. It still works, but the hooks above are simpler and type better.

See: connect

References

0 Comments for this cheatsheet. Write yours!