Redux Toolkit 19 ⚛️ TypeScript State — Typing root state and dispatch hooks
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
| Type | Derivation | Purpose |
|---|---|---|
RootState | ReturnType<typeof store.getState> | State shape in selectors |
AppDispatch | typeof store.dispatch | Dispatch including thunks |
Typed hooks
| Hook | React Redux 9.1+ | Pre-9.1 |
|---|---|---|
| Selector | useSelector.withTypes<RootState>() | useSelector((s: RootState) => ...) |
| Dispatch | useDispatch.withTypes<AppDispatch>() | useDispatch<AppDispatch>() |
| Store | useStore.withTypes<RootState>() | useStore<RootState>() |
File structure
| File | Exports |
|---|---|
store.ts | store, RootState, AppDispatch |
hooks.ts | useAppSelector, useAppDispatch, useAppStore |
| Component | Imports from hooks.ts |
Common type patterns
| Pattern | Example |
|---|---|
| Selector | useAppSelector((state) => state.todos.items) |
| Dispatch action | dispatch(toggleTodo(id)) |
| Dispatch thunk | dispatch(async (d, g) => {}) |
| Class props | WithRedux & { 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
state is unknown in selector | Raw useSelector without type | Use useAppSelector |
| Thunk dispatch type error | Plain Dispatch used | Use useAppDispatch |
RootState out of sync | Manually written interface | Derive from store.getState |
| Circular import error | Component imports store types directly | Import typed hooks from hooks.ts |
useStore returns unknown | No type parameter | Use 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
| Item | Value |
|---|---|
RootState | ReturnType<typeof store.getState> |
AppDispatch | typeof store.dispatch |
| Typed selector | useSelector.withTypes<RootState>() |
| Typed dispatch | useDispatch.withTypes<AppDispatch>() |
| Typed store | useStore.withTypes<RootState>() |
| Hooks file | app/hooks.ts |
| Pre-9.1 pattern | Explicit generic wrappers |
| Circular import fix | Import hooks, not store types |
Key takeaways:
RootStateandAppDispatchare derived, not written.RootStateis the return type ofstore.getState.AppDispatchis the type ofstore.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.
useAppSelectorknows the state shape;useAppDispatchaccepts thunks. Components import these hooks instead of the rawuseSelectoranduseDispatch, and the type inference works without manual annotations . AppDispatchis notDispatch. The plainDispatchtype does not include the thunk middleware. A component that usesDispatchcannot dispatch a thunk without a type error.AppDispatchcaptures 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
useStorehook follows the same pattern.useStore.withTypes<RootState>()narrows thegetState()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.
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!