| | |

Redux Toolkit 19 ⚛️ TypeScript State — Typing root state and dispatch hooks

⏱️ Estimated read: 11 min | |

A Redux store in a TypeScript project has two types that every component needs: the shape of the state (RootState) and the type of the dispatch function (AppDispatch). Neither type exists by default. RootState is derived from the store’s reducer map, and AppDispatch is derived from the store itself, because the store’s dispatch function includes the thunk middleware’s extended signature. The standard pattern is to export both types from the store file and create typed versions of the useSelector and useDispatch hooks that use them.

This chapter covers the derivation of RootState and AppDispatch, the typed hook pattern that replaces the untyped useSelector and useDispatch in components, the useAppSelector and useAppDispatch exports, and the circular import problem that the pattern solves. It also covers the typed useStore hook and the WithRedux type helper for class components.

Key point: RootState is ReturnType<typeof store.getState>. AppDispatch is typeof store.dispatch. Export both from the store file and use them in the typed hooks. Do not annotate them manually; the derivation is what keeps them in sync with the store’s actual shape.


Why the typed hooks exist

The inference problem. useSelector and useDispatch are generic functions, but without a type argument they infer unknown or Dispatch<any>. A component that calls useSelector((state) => state.todos) receives unknown, and every property access on the result is a TypeScript error. The component must annotate the state parameter manually, which duplicates the type in every component and drifts from the store’s actual shape .

The thunk problem. The store’s dispatch function is not the plain Dispatch from Redux. configureStore adds the thunk middleware, which extends dispatch to accept thunks. AppDispatch captures that extended signature. A component that uses the plain Dispatch type cannot dispatch a thunk without a type error, because the plain type does not know about the thunk middleware .

The circular import problem. The typed hooks are defined in a file that imports RootState and AppDispatch from the store. The store file imports the reducers from the slices. The slices do not import the store. The components import the typed hooks. This dependency graph is acyclic. If the components imported RootState directly from the store and the store imported a component’s type, a cycle would form. The typed hooks file breaks the cycle by being the single place that connects the store types to the hooks.


a. Deriving RootState and AppDispatch

The store file exports the store, RootState, and AppDispatch. The types are derived, not written by hand.

// app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from '../features/todos/todosSlice'
import usersReducer from '../features/users/usersSlice'

export const store = configureStore({
  reducer: {
    todos: todosReducer,
    users: usersReducer,
  },
})

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

RootState is the return type of store.getState. It reflects the exact shape of the combined reducer map: { todos: TodosState; users: UsersState }. AppDispatch is the type of store.dispatch, which includes the thunk middleware’s extension. Both types update automatically when the reducer map changes .


b. The typed hooks file

The typed hooks file imports RootState and AppDispatch and creates typed versions of useSelector and useDispatch.

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

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

The withTypes method, introduced in React Redux 9.1, is a static method on the hook that returns a new hook with the type parameter pre-applied. This is the recommended pattern for React Redux 9. For earlier versions, the pattern uses explicit generic wrappers:

// app/hooks.ts (React Redux < 9.1)
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector = <T>(selector: (state: RootState) => T) => useSelector(selector)

Both patterns produce the same result: a useAppSelector that knows the state shape and a useAppDispatch that accepts thunks. Components import these hooks instead of the raw useSelector and useDispatch .


c. Using the typed hooks in components

A component that imports useAppSelector gets full inference on the state parameter. A component that imports useAppDispatch can dispatch thunks and actions without type annotations.

// features/todos/TodoList.tsx
import { useAppSelector, useAppDispatch } from '../../app/hooks'
import { toggleTodo } from './todosSlice'

export function TodoList() {
  const todos = useAppSelector((state) => state.todos.items)
  const dispatch = useAppDispatch()

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => dispatch(toggleTodo(todo.id))}>
          {todo.title}
        </li>
      ))}
    </ul>
  )
}

The state parameter in the selector is typed as RootState, so state.todos.items is checked against the actual slice state. The dispatch function accepts toggleTodo(todo.id), which returns an action, and would also accept a thunk if the component dispatched one.

The useStore hook follows the same pattern:

// app/hooks.ts
import { useStore } from 'react-redux'
import type { RootState } from './store'

export const useAppStore = useStore.withTypes<RootState>()

The useStore hook returns the store instance, and the typed version narrows getState() to RootState.


Complete Example Session

// ============================================
// PART 1: THE STORE
// ============================================
// app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from '../features/todos/todosSlice'
import usersReducer from '../features/users/usersSlice'

export const store = configureStore({
  reducer: {
    todos: todosReducer,
    users: usersReducer,
  },
})
// ============================================
// PART 2: THE DERIVED TYPES
// ============================================
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

// RootState: { todos: TodosState; users: UsersState }
// AppDispatch: typeof store.dispatch (includes thunk)
// ============================================
// PART 3: THE TYPED HOOKS (REACT REDUX 9.1+)
// ============================================
// app/hooks.ts
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<RootState>()
// ============================================
// PART 4: THE TYPED HOOKS (PRE-9.1)
// ============================================
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector = <T>(selector: (state: RootState) => T) => useSelector(selector)
// ============================================
// PART 5: USING THE TYPED SELECTOR
// ============================================
import { useAppSelector } from '../../app/hooks'

const todos = useAppSelector((state) => state.todos.items)
// state is typed as RootState
// state.todos.items is typed as Todo[]
// ============================================
// PART 6: USING THE TYPED DISPATCH
// ============================================
import { useAppDispatch } from '../../app/hooks'
import { toggleTodo } from './todosSlice'

const dispatch = useAppDispatch()
dispatch(toggleTodo('1'))
// dispatch accepts actions and thunks
// ============================================
// PART 7: DISPATCHING A THUNK
// ============================================
const dispatch = useAppDispatch()

dispatch(async (dispatch, getState) => {
  const state = getState() // typed as RootState
  // ...
})

// AppDispatch includes the thunk signature.
// ============================================
// PART 8: THE CIRCULAR IMPORT PROBLEM
// ============================================
// components import hooks.ts
// hooks.ts imports store.ts
// store.ts imports slices
// slices do NOT import store.ts
//
// The dependency graph is acyclic.
// hooks.ts is the bridge between store types and components.
// ============================================
// PART 9: THE WithRedux HELPER FOR CLASS COMPONENTS
// ============================================
import type { RootState, AppDispatch } from './store'

export type WithRedux = {
  state: RootState
  dispatch: AppDispatch
}

// Use in a class component:
// type Props = WithRedux & { id: string }
// ============================================
// PART 10: THE COMPLETE COMPONENT
// ============================================
import { useAppSelector, useAppDispatch } from '../../app/hooks'
import { toggleTodo } from './todosSlice'

export function TodoList() {
  const todos = useAppSelector((state) => state.todos.items)
  const dispatch = useAppDispatch()

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => dispatch(toggleTodo(todo.id))}>
          {todo.title}
        </li>
      ))}
    </ul>
  )
}

The ten parts covered the store, the derived types, the typed hooks for React Redux 9.1+, the pre-9.1 pattern, the typed selector, the typed dispatch, thunk dispatch, the circular import solution, the WithRedux helper, and the complete component.


Quick Reference

Type derivation

TypeDerivationPurpose
RootStateReturnType<typeof store.getState>State shape in selectors
AppDispatchtypeof store.dispatchDispatch including thunks

Typed hooks

HookReact Redux 9.1+Pre-9.1
SelectoruseSelector.withTypes<RootState>()useSelector((s: RootState) => ...)
DispatchuseDispatch.withTypes<AppDispatch>()useDispatch<AppDispatch>()
StoreuseStore.withTypes<RootState>()useStore<RootState>()

File structure

FileExports
store.tsstore, RootState, AppDispatch
hooks.tsuseAppSelector, useAppDispatch, useAppStore
ComponentImports from hooks.ts

Common type patterns

PatternExample
SelectoruseAppSelector((state) => state.todos.items)
Dispatch actiondispatch(toggleTodo(id))
Dispatch thunkdispatch(async (d, g) => {})
Class propsWithRedux & { id: string }

Best Practices

✅ Do This:

// Derive types from the store
export type RootState = ReturnType<typeof store.getState>  // ✅
// Export typed hooks from a dedicated file
export const useAppSelector = useSelector.withTypes<RootState>() // ✅
// Import the typed hooks in components
import { useAppSelector } from '../../app/hooks'            // ✅
// Use AppDispatch for thunk dispatch
const dispatch = useAppDispatch()                           // ✅

❌ Don’t Do This:

// Manually annotate RootState
interface RootState { todos: Todo[] } // ❌ (drifts from store)
// Use raw useSelector without typing
const todos = useSelector((state) => state.todos)          // ❌ (state is unknown)
// Use plain Dispatch for thunks
const dispatch: Dispatch = useDispatch()                   // ❌ (rejects thunks)
// Import RootState directly in every component
// Use the typed hook instead                                 // ❌

Common Pitfalls

PitfallWhy It HappensFix
state is unknown in selectorRaw useSelector without typeUse useAppSelector
Thunk dispatch type errorPlain Dispatch usedUse useAppDispatch
RootState out of syncManually written interfaceDerive from store.getState
Circular import errorComponent imports store types directlyImport typed hooks from hooks.ts
useStore returns unknownNo type parameterUse useStore.withTypes<RootState>()

Real-World Examples

1. Store with derived types

export const store = configureStore({ reducer })
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

2. Typed selector hook

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

3. Typed dispatch hook

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

4. Typed store hook

export const useAppStore = useStore.withTypes<RootState>()

5. Selector usage

const todos = useAppSelector((state) => state.todos.items)

6. Action dispatch

dispatch(toggleTodo('1'))

7. Thunk dispatch

dispatch(async (dispatch, getState) => {})

8. Class component props

type Props = WithRedux & { id: string }

9. Pre-9.1 selector

export const useAppSelector = <T>(selector: (state: RootState) => T) => useSelector(selector)

10. Pre-9.1 dispatch

export const useAppDispatch = () => useDispatch<AppDispatch>()

Visual

The type derivation flow

┌─────────────────────────────────────────────────────────────┐
│  TYPE DERIVATION                                             │
│                                                              │
│  configureStore({ reducer: { todos, users } })              │
│       │                                                      │
│       ▼                                                      │
│  store                                                       │
│       │                                                      │
│       ├── store.getState() → { todos, users }               │
│       │     → RootState = ReturnType<typeof store.getState> │
│       │                                                      │
│       └── store.dispatch → thunk-extended dispatch          │
│             → AppDispatch = typeof store.dispatch            │
└─────────────────────────────────────────────────────────────┘

The hooks file as a bridge

┌─────────────────────────────────────────────────────────────┐
│  DEPENDENCY GRAPH                                            │
│                                                              │
│  store.ts                                                    │
│    exports: store, RootState, AppDispatch                    │
│       │                                                      │
│       ▼                                                      │
│  hooks.ts                                                    │
│    imports: RootState, AppDispatch                           │
│    exports: useAppSelector, useAppDispatch                   │
│       │                                                      │
│       ▼                                                      │
│  components                                                  │
│    import: useAppSelector, useAppDispatch                    │
│                                                              │
│  No cycle. hooks.ts is the single bridge.                    │
└─────────────────────────────────────────────────────────────┘

The thunk dispatch difference

┌─────────────────────────────────────────────────────────────┐
│  DISPATCH TYPES                                              │
│                                                              │
│  Dispatch (plain Redux):                                     │
│    (action: AnyAction) => AnyAction                          │
│    → rejects thunks                                          │
│                                                              │
│  AppDispatch (configureStore):                               │
│    (action: AnyAction | ThunkAction) => AnyAction | Promise  │
│    → accepts thunks and actions                              │
│                                                              │
│  useAppDispatch uses AppDispatch.                            │
└─────────────────────────────────────────────────────────────┘

Component usage

┌─────────────────────────────────────────────────────────────┐
│  COMPONENT                                                   │
│                                                              │
│  import { useAppSelector, useAppDispatch } from './hooks'   │
│       │                                                      │
│       ├── const todos = useAppSelector(s => s.todos.items)  │
│       │     → s is RootState, todos is Todo[]               │
│       │                                                      │
│       └── const dispatch = useAppDispatch()                  │
│             → dispatch accepts actions and thunks            │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
RootStateReturnType<typeof store.getState>
AppDispatchtypeof store.dispatch
Typed selectoruseSelector.withTypes<RootState>()
Typed dispatchuseDispatch.withTypes<AppDispatch>()
Typed storeuseStore.withTypes<RootState>()
Hooks fileapp/hooks.ts
Pre-9.1 patternExplicit generic wrappers
Circular import fixImport hooks, not store types

Key takeaways:

  • RootState and AppDispatch are derived, not written. RootState is the return type of store.getState. AppDispatch is the type of store.dispatch, which includes the thunk middleware’s extension. Deriving them keeps them in sync with the store’s actual shape .
  • The typed hooks are the components’ interface to the store. useAppSelector knows the state shape; useAppDispatch accepts thunks. Components import these hooks instead of the raw useSelector and useDispatch, and the type inference works without manual annotations .
  • AppDispatch is not Dispatch. The plain Dispatch type does not include the thunk middleware. A component that uses Dispatch cannot dispatch a thunk without a type error. AppDispatch captures the extended signature .
  • The hooks file breaks the circular import. The store imports the slices. The hooks import the store types. The components import the hooks. Nothing imports the components from the store, so the dependency graph is acyclic .
  • React Redux 9.1 introduced withTypes. The static method returns a new hook with the type parameter pre-applied. It is the recommended pattern for new code. The explicit generic wrappers work for earlier versions.
  • The useStore hook follows the same pattern. useStore.withTypes<RootState>() narrows the getState() return type. It is used less often than the other two hooks, but the typing is identical.

Remember: The typed hooks pattern is a one-time setup that eliminates type annotations from every component. Derive RootState and AppDispatch from the store, create useAppSelector and useAppDispatch in a hooks file, and import those hooks everywhere. The derivation is what keeps the types correct as the store’s reducer map changes. Manual interfaces drift. The typed hooks do not.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!