Skip to content
D
Documentation

Middleware and composition

concept
2 min readUpdated

Middleware wraps a state creator to extend how one Zustand store behaves, while slices let you build that store from smaller state creators.

How the pieces fit

create receives the composed state creator and returns one hook-backed store. A middleware such as devtools wraps that creator; slice creators contribute their fields and actions to the same store.

mermaid
flowchart TD
  A["create()"] --> B["devtools()"]
  B --> C["Composed state creator"]
  C --> D["Bear slice"]
  C --> E["Fish slice"]
  C --> F["One store and hook"]

The state creator is the function that receives set, get, and the store API and returns initial state. The StateCreator type lets each slice return only its own fields while typing its arguments against the full store state. Compose the slices into one state creator, then apply middleware to that combined creator—not separately inside each slice.

Compose slices and wrap the store

Use slices when a growing store benefits from splitting its state and actions by feature. This browser example puts bear and fish state in separate creators, wraps the combined creator with devtools, and mounts a React component that displays both counts. Clicking either button updates the matching count in the same store.

tsx
import { create, type StateCreator } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createRoot } from 'react-dom/client'

interface BearSlice {
  bears: number
  addBear: () => void
  eatFish: () => void
}

interface FishSlice {
  fishes: number
  addFish: () => void
}

type StoreState = BearSlice & FishSlice

const createBearSlice: StateCreator<
  StoreState,
  [['zustand/devtools', never]],
  [],
  BearSlice
> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})

const createFishSlice: StateCreator<
  StoreState,
  [['zustand/devtools', never]],
  [],
  FishSlice
> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

export const useBoundStore = create<StoreState>()(
  devtools((...args) => ({
    ...createBearSlice(...args),
    ...createFishSlice(...args),
  })),
)

export function App() {
  const bears = useBoundStore((state) => state.bears)
  const fishes = useBoundStore((state) => state.fishes)
  const addBear = useBoundStore((state) => state.addBear)
  const addFish = useBoundStore((state) => state.addFish)

  return (
    <main>
      <p>Bears: {bears}</p>
      <p>Fish: {fishes}</p>
      <button onClick={addBear}>Add bear</button>
      <button onClick={addFish}>Add fish</button>
    </main>
  )
}

const container = document.createElement('div')
document.body.append(container)
createRoot(container).render(<App />)

The eatFish action demonstrates that a slice can update a field owned by another slice: each creator receives the full store state through its typed set argument. The component selects individual values and actions from the combined hook, so the rendered counts reflect updates to the shared store.

Middleware composition and typing

Apply middleware at the combined-store boundary. Putting middleware in individual slices can cause unexpected issues. With multiple middleware, nest the wrappers directly inside create so TypeScript can infer their mutator types. For example, devtools(persist(stateCreator, options)) composes persistence inside DevTools. Put devtools outermost in the chain because other middleware can modify setState; placing devtools last ensures its setState changes and added type parameter remain in place.

When a slice uses middleware, include the middleware mutator tuple in its StateCreator type, as the example does for devtools. Keep middleware nesting directly inside create; a reusable function that bundles middleware requires more advanced types to preserve contextual inference.

When to use combine

combine is an alternative when you want the initial-state object and the action creator in one place and want TypeScript to infer the state. It merges the initial object and the creator's returned object shallowly. The authors caution that the creator's get and set types reflect the initial-state parameter rather than the merged result, so replacing state or relying on Object.keys(get()) can be unsafe. Prefer typed slices when the store is organized into separate feature creators or when you want the full state type available to each slice.

Was this page helpful?