Skip to content
D
Documentation

Typed store patterns

concept
3 min readUpdated

This page adds the type patterns for reusing a creator, extracting a store's complete shape, and carrying custom middleware changes through composition. For the roles of StateCreator, StoreApi, UseBoundStore, ExtractState, create, and createStore, see How Zustand works.

The model

A typed store has three related layers:

mermaid
flowchart LR
  T["State and actions: T"] --> C["StateCreator<T>"]
  C --> R["create() or createStore()"]
  R --> A["UseBoundStore or StoreApi<T>"]
  A --> X["ExtractState<typeof store>"]
  C --> M["Middleware mutator tuples"]
  M --> R

The addition here is the type flow: one creator can feed either binding, and ExtractState<typeof store> can recover the complete state-and-actions shape after the store is created. For the initializer arguments, bindings, and vanilla API, see How Zustand works.

The type parameter is important because the state type is used both as an input to get and as the initializer's result. TypeScript cannot reliably infer that invariant type from the initializer alone, so use the curried form when you provide the state type: create<T>()((set) => ...) or createStore<T>()((set) => ...).

Define the creator once

Give the state and actions a single type, then export a creator that can be used by either a React-bound or vanilla store:

ts
import { type StateCreator } from 'zustand'

export type CounterStore = {
  count: number
  increment: () => void
}

export const counterStoreCreator: StateCreator<CounterStore> = (set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
})

The creator returns both data and actions. set performs a shallow merge by default, so the action returns only the changed field. Treat nested objects and arrays as immutable: create a new nested value before passing it to set.

Bind the creator in React

Pass the creator to the curried form of create. The result is a callable store whose selector is typed as CounterStore:

tsx
import { create } from 'zustand'
import { type CounterStore, counterStoreCreator } from './counter-store'

const useCounterStore = create<CounterStore>()(counterStoreCreator)

export function Counter() {
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)

  return (
    <button type="button" onClick={increment}>
      Count: {count}
    </button>
  )
}

The button reads one atomic value and one action. Clicking it updates the store and the component re-renders because its selected count changes.

When a component needs several values, select them with a stable result or use useShallow from zustand/react/shallow; avoid subscribing to the whole store when only part of it is needed.

Infer the store state

Use ExtractState when a utility, test, or component prop needs the complete state-and-actions shape of an existing store. It reads the return type of getState() rather than making you repeat the type:

ts
import { create, type ExtractState } from 'zustand'

type CounterStateShape = {
  count: number
  increment: () => void
}

const useCounterStore = create<CounterStateShape>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

export type CounterState = ExtractState<typeof useCounterStore>

export function formatCounter(state: CounterState): string {
  return `Count: ${state.count}`
}

export const currentCounter = formatCounter(useCounterStore.getState())

CounterState includes count and increment. The store itself remains a UseBoundStore, so getState() returns that same inferred shape.

Use a vanilla store with a React binding

Use createStore when the store must exist independently of React—for example, when you create a scoped store for a particular owner. Bind that store in a component with useStore:

tsx
import { useState } from 'react'
import { createStore, type StoreApi } from 'zustand'
import { useStore } from 'zustand'
import { type CounterStore, counterStoreCreator } from './counter-store'

function createCounterStore(): StoreApi<CounterStore> {
  return createStore<CounterStore>()(counterStoreCreator)
}

export function ScopedCounter() {
  const [counterStore] = useState(createCounterStore)
  const count = useStore(counterStore, (state) => state.count)
  const increment = useStore(counterStore, (state) => state.increment)

  return (
    <button type="button" onClick={increment}>
      Scoped count: {count}
    </button>
  )
}

The store is created once for this component instance. useStore subscribes to the vanilla API and returns the selected value, while the StoreApi<CounterStore> annotation keeps direct store access typed outside the component.

Compose middleware without losing the state type

For middleware that wraps a creator, compose the shipped middleware directly inside create. This keeps the initializer contextually typed:

ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

interface CounterState {
  count: number
  increment: () => void
}

export const usePersistedCounter = create<CounterState>()(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set((state) => ({ count: state.count + 1 })),
      }),
      { name: 'counter-store' },
    ),
  ),
)

The store keeps CounterState while persist receives its storage name and devtools wraps the resulting creator. Keep this nesting immediately inside create; moving it into an untyped helper loses the contextual inference that supplies the creator's set type.

For the general roles of Mutate, StateCreator, StoreMutators, and StoreMutatorIdentifier, see How Zustand works. This page adds the custom-middleware pattern: use Mis for mutators already present on the creator's input and Mos for mutators it produces, then make the declaration merge and runtime implementation agree:

ts
import type {
  Mutate,
  StateCreator,
  StoreApi,
  StoreMutatorIdentifier,
  StoreMutators,
} from 'zustand'

declare module 'zustand/vanilla' {
  interface StoreMutators<S, A> {
    counterLabel: S & { counterLabel: A }
  }
}

type CounterLabel = <
  T,
  A,
  Mps extends [StoreMutatorIdentifier, unknown][] = [],
  Mos extends [StoreMutatorIdentifier, unknown][] = [],
>(
  creator: StateCreator<T, [...Mps, ['counterLabel', A]], Mos>,
  label: A,
) => StateCreator<T, Mps, [['counterLabel', A], ...Mos]>

type LabeledCounterApi = Mutate<
  StoreApi<CounterStore>,
  [['counterLabel', string]]
>

type CounterStore = {
  count: number
}

The declaration describes the type-level contract; a working middleware also has to implement the corresponding runtime change. Do not use this extension pattern when ordinary shipped middleware already provides the behavior.

Choose the pattern

  • Use create<T>() when a React component can consume one global bound store.
  • Use ExtractState<typeof store> when another type needs the store's complete inferred shape.
  • Use createStore<T>() and useStore(store, selector) when store creation must be separate from React or scoped to an owner.
  • Nest shipped middleware directly inside create so contextual typing reaches every creator.
  • Use mutator tuples and declaration merging only when you are implementing middleware that changes the store API.

For immutable update details, see Immutable state updates. For vanilla-store lifecycles, see Vanilla and scoped stores. For middleware ordering and composition, see Compose store middleware.

Was this page helpful?