mrkeyoor.com_
Thu 06 Aug 07:43 UTC
npmWeb Frontendupdated 06 Aug 2026

@reduxjs/toolkit

Redux Toolkit (RTK) is the official Redux package, and it exists because raw Redux made you hand-write action type constants, action creators, switch-statement reducers, and store wiring for every feature. RTK replaces all of that with createSlice, which takes a name, an initial state, and an object of reducer functions, then hands back the reducer plus matching action creators. configureStore sets up the store with the thunk middleware, the Redux DevTools connection, and development-only checks for accidental mutation and non-serializable state already turned on. Immer is built in, so inside a reducer you write state.items.push(item) and get a correctly immutable update out. The package also bundles RTK Query, a separate opt-in entry point that handles data fetching, caching, request deduplication, and cache invalidation, and generates React hooks for each endpoint you declare.

Verdict

If you are writing Redux, write it with RTK; the hand-rolled alternative is strictly worse. The real decision is upstream of that, because plenty of apps that reach for Redux would be better served by zustand for client state and TanStack Query for server data.

API stability5/52.0 landed at the end of 2023 with the breaking changes people still hit (object extraReducers removed, no default exports), and the 2.x line since then has been additive: infinite queries, exported hook option types, and internal performance work, with no forced rewrites
Docs5/5redux-toolkit.js.org has a full API reference plus usage guides, and the Redux core docs carry the Essentials and Fundamentals tutorials that walk an app end to end; 2.12 even ships agent skill files inside the package for migration and usage
Maintenance5/5Pushed today, releases every few weeks, detailed release notes explaining each change, and the maintainers publish profiling work behind their Immer upgrades; around 213 open issues on a repo this size, and publishing moved to npm Trusted Publishing in 2.11.1
Ecosystem5/5Around 26M downloads a week, official Vite and Next.js templates, an OpenAPI codegen for RTK Query, and the Redux DevTools extension; nearly every React state-management tutorial written since 2020 assumes it

Use it if

  • You already have Redux in the app: RTK is the officially recommended way to write it and cuts the per-feature boilerplate to one createSlice call
  • Many unrelated components read and write the same state, and you want one inspectable store plus DevTools time-travel rather than prop drilling or a pile of contexts
  • You want server data caching and global client state from one dependency, since RTK Query ships inside the same package and stores its cache in the same store
  • Your team wants strong TypeScript inference without writing action union types by hand, which createSlice and the builder callback give you
  • You need middleware-level control over side effects, whether that is createListenerMiddleware for reactive logic or your own middleware for logging and analytics
Skip it if

Setup reality

npm install @reduxjs/toolkit react-redux gets you running, and TypeScript types are bundled with no @types package needed. React and react-redux are optional peer dependencies, so RTK installs cleanly in a Node script, but the moment you use the React bindings your package manager will complain if react-redux is missing. Six runtime dependencies come along including redux, immer, reselect, and redux-thunk, which is normal but does mean your lockfile grows. The setup that actually takes time is typing: you export RootState and AppDispatch from the store file, then build typed useAppSelector and useAppDispatch hooks with react-redux's withTypes helpers, and every tutorial older than RTK 2.0 shows a pattern that no longer compiles. Two more edges worth knowing before you start: the object form of extraReducers was removed in 2.0 so it is builder callbacks only, and RTK Query lives at separate entry points (@reduxjs/toolkit/query and @reduxjs/toolkit/query/react) rather than the package root.

Patterns

Create the store and export its typesconfigure-store

// app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from '../features/todos/todosSlice'
import filtersReducer from '../features/filters/filtersSlice'

export const store = configureStore({
  reducer: {
    todos: todosReducer,
    filters: filtersReducer,
  },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

configureStore combines the reducer object for you, adds redux-thunk, wires up DevTools, and turns on the immutability and serializability checks in development only. Deriving RootState from getState rather than declaring it by hand is what keeps selectors typed as slices come and go.

Define a slice with reducers and selectorscreate-slice

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

interface Todo { id: string; text: string; done: boolean }

const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [] as Todo[] },
  reducers: {
    added(state, action: PayloadAction<Todo>) {
      state.items.push(action.payload)
    },
    toggled(state, action: PayloadAction<string>) {
      const todo = state.items.find((t) => t.id === action.payload)
      if (todo) todo.done = !todo.done
    },
    cleared(state) {
      state.items = []
    },
  },
  selectors: {
    selectAll: (state) => state.items,
    selectRemaining: (state) => state.items.filter((t) => !t.done).length,
  },
})

export const { added, toggled, cleared } = todosSlice.actions
export const { selectAll, selectRemaining } = todosSlice.selectors
export default todosSlice.reducer

The mutating code is Immer draft syntax, not real mutation. Never both mutate the draft and return a value from the same reducer: Immer throws on that. slice.selectors are written against the slice state and get rooted at slice.name automatically.

Build typed useSelector and useDispatchtyped-hooks

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

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

withTypes is the react-redux 9 form and replaces the older `useDispatch: () => AppDispatch` alias pattern you will still find in tutorials. The typed dispatch is what lets you dispatch a thunk without a TypeScript error.

Fetch with createAsyncThunk and handle its lifecycleasync-thunk

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

export const fetchUser = createAsyncThunk(
  'users/fetchById',
  async (userId: string, { rejectWithValue, signal }) => {
    const res = await fetch(`/api/users/${userId}`, { signal })
    if (!res.ok) return rejectWithValue({ status: res.status })
    return (await res.json()) as User
  },
  {
    condition: (userId, { getState }) => {
      const { users } = getState() as RootState
      return !users.byId[userId]  // skip if already cached
    },
  },
)

const usersSlice = createSlice({
  name: 'users',
  initialState: { byId: {} as Record<string, User>, loading: false, error: null as unknown },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => { state.loading = true })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false
        state.byId[action.payload.id] = action.payload
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false
        state.error = action.payload ?? action.error.message
      })
  },
})

The object form of extraReducers was removed in RTK 2.0, so the builder callback is the only option. Returning rejectWithValue puts your data on action.payload; a thrown error lands on action.error instead, which is why the rejected case checks both.

Declare an API slice and use the generated hooksrtk-query-queries

// services/pokemon.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api/',
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token
      if (token) headers.set('authorization', `Bearer ${token}`)
      return headers
    },
  }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => 'posts',
      providesTags: ['Post'],
    }),
    getPost: build.query<Post, string>({
      query: (id) => `posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }],
    }),
  }),
})

export const { useGetPostsQuery, useGetPostQuery } = api

// store.ts
// reducer: { [api.reducerPath]: api.reducer },
// middleware: (gDM) => gDM().concat(api.middleware),

Forgetting to add api.middleware is the classic RTK Query bug: hooks stay stuck in the loading state with no error. Import from @reduxjs/toolkit/query/react for the hooks, or @reduxjs/toolkit/query for the framework-free version.

Mutate and invalidate the cachertk-query-mutations

addPost: build.mutation<Post, Partial<Post>>({
  query: (body) => ({ url: 'posts', method: 'POST', body }),
  invalidatesTags: ['Post'],
}),

// component
const [addPost, { isLoading, error }] = useAddPostMutation()

async function onSubmit(values: Partial<Post>) {
  try {
    const created = await addPost(values).unwrap()
    navigate(`/posts/${created.id}`)
  } catch (err) {
    // err is the serialized error, not a thrown Response
  }
}

Mutation triggers resolve with a result object rather than rejecting, so call .unwrap() when you want a try/catch. invalidatesTags matched against providesTags is what triggers the refetch; get the tag names wrong and nothing happens, silently.

Normalize a collection with createEntityAdapterentity-adapter

import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'

const booksAdapter = createEntityAdapter<Book>({
  sortComparer: (a, b) => a.title.localeCompare(b.title),
})

const booksSlice = createSlice({
  name: 'books',
  initialState: booksAdapter.getInitialState({ loading: false }),
  reducers: {
    bookAdded: booksAdapter.addOne,
    booksReceived: booksAdapter.setAll,
    bookUpdated: booksAdapter.updateOne,
    bookRemoved: booksAdapter.removeOne,
  },
})

export const { selectAll: selectAllBooks, selectById: selectBookById } =
  booksAdapter.getSelectors((state: RootState) => state.books)

The adapter keeps { ids, entities } in state so lookups are O(1) instead of Array.find. updateOne takes { id, changes } rather than a whole entity, which trips people up on the first try.

Derive data without re-rendering on every dispatchmemoized-selector

import { createSelector } from '@reduxjs/toolkit'

export const selectVisibleTodos = createSelector(
  [(state: RootState) => state.todos.items, (state: RootState) => state.filters.status],
  (items, status) =>
    status === 'all' ? items : items.filter((t) => (status === 'done' ? t.done : !t.done)),
)

Any selector that builds a new array or object needs memoizing, because useSelector compares with reference equality and a fresh array every call means a re-render on every dispatch. createSelector is Reselect, re-exported by RTK.

React to actions without sagaslistener-middleware

import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit'

export const listenerMiddleware = createListenerMiddleware()

listenerMiddleware.startListening({
  matcher: isAnyOf(todoAdded, todoToggled),
  effect: async (action, listenerApi) => {
    listenerApi.cancelActiveListeners()
    await listenerApi.delay(500)  // debounce
    const state = listenerApi.getState() as RootState
    localStorage.setItem('todos', JSON.stringify(state.todos.items))
  },
})

// store.ts
// middleware: (gDM) => gDM().prepend(listenerMiddleware.middleware),

Use prepend rather than concat so the listener sees actions before other middleware. cancelActiveListeners plus delay is the supported debounce; the effect is cancelled by throwing internally, so do not swallow errors around await points.

Generate part of the payload inside the action creatorprepare-payload

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

const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [] as Todo[] },
  reducers: {
    added: {
      reducer(state, action: PayloadAction<Todo>) {
        state.items.push(action.payload)
      },
      prepare(text: string) {
        return { payload: { id: nanoid(), text, done: false } }
      },
    },
  },
})

Reducers have to stay pure, so nanoid() and Date.now() belong in prepare, not in the reducer body. prepare must return an object with a payload key; returning the payload directly is a common mistake.

Inject slices after the store is createdlazy-load-slices

import { combineSlices, configureStore } from '@reduxjs/toolkit'
import { todosSlice } from './features/todos/todosSlice'

const rootReducer = combineSlices(todosSlice)
export const store = configureStore({ reducer: rootReducer })
export type RootState = ReturnType<typeof rootReducer>

// in a lazily loaded route bundle
const injected = rootReducer.inject(adminSlice)
const selectAdmin = injected.selector((state) => state.admin)

Code splitting a large app means the store cannot know every reducer up front. combineSlices keeps RootState typed as the injected slices are optional, so downstream selectors have to handle the slice being undefined before injection.

Tune the development checks instead of fighting themconfigure-middleware-checks

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['file/uploadStarted'],
        ignoredPaths: ['upload.fileHandle'],
      },
      immutableCheck: { warnAfter: 128 },
    }).concat(api.middleware),
})

Both checks are development-only and both walk your state on every action, so a large store makes dispatches visibly slow in dev. Reach for these options only after deciding the non-serializable value genuinely belongs in the store.

Alternatives

PackageRegistryPick it when
zustandnpmYou want a small global store with hooks and no providers, actions, or middleware pipeline
@tanstack/react-querynpmYour state is mostly server data and you want caching, refetching, and mutations without adopting Redux at all
jotainpmYou prefer bottom-up atomic state that re-renders only the components reading a given atom