Skip to content
D
Documentation

Split a store into slices

how-to
2 min readUpdated

Use slices when your global store grows too large to maintain in one state creator. Keep state and actions together in each slice, then compose the slice creators into one store. This example renders bear and fish counts, calls actions across slices, and adds persistence at the combined-store boundary.

1. Define typed slice creators

For slices, use StateCreator with the combined BoundState as its first type argument and the individual slice's return type as its fourth; see How Zustand works for state creators and their set and get arguments.

Create this file in your React TypeScript project:

ts
import type { StateCreator } from 'zustand'

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

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

export type SharedSlice = {
  addBoth: () => void
  getBoth: () => number
}

export type BoundState = BearSlice & FishSlice & SharedSlice

export const createBearSlice: StateCreator<BoundState, [], [], BearSlice> = (
  set,
) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})

export const createFishSlice: StateCreator<BoundState, [], [], FishSlice> = (
  set,
) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

export const createSharedSlice: StateCreator<BoundState, [], [], SharedSlice> = (
  _set,
  get,
) => ({
  addBoth: () => {
    get().addBear()
    get().addFish()
  },
  getBoth: () => get().bears + get().fishes,
})

eatFish() updates a field owned by the fish slice. addBoth() reads the combined store with get() and calls both slices' actions. Each action returns void; getBoth() returns the current sum as a number. The two calls inside addBoth() perform two store updates, not one atomic update.

2. Compose one hook and render its state

Use create with create<BoundState>()(...). Pass the same set, get, and store arguments to every slice creator, then spread their results into one state object. The result is a bound store hook, not a separate store for each slice.

ts
import { create } from 'zustand'
import {
  createBearSlice,
  createFishSlice,
  createSharedSlice,
  type BoundState,
} from './slices'

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

Select each count and action from that hook. This browser entry point mounts the component into a sized container:

tsx
import { createRoot } from 'react-dom/client'
import { useBoundStore } from './store'

function App() {
  const bears = useBoundStore((state) => state.bears)
  const fishes = useBoundStore((state) => state.fishes)
  const total = useBoundStore((state) => state.bears + state.fishes)
  const addBear = useBoundStore((state) => state.addBear)
  const addFish = useBoundStore((state) => state.addFish)
  const eatFish = useBoundStore((state) => state.eatFish)
  const addBoth = useBoundStore((state) => state.addBoth)

  return (
    <main>
      <h1>Bear and fish store</h1>
      <p>Number of bears: {bears}</p>
      <p>Number of fishes: {fishes}</p>
      <p>Total: {total}</p>
      <button onClick={addBear}>Add a bear</button>
      <button onClick={addFish}>Add a fish</button>
      <button onClick={eatFish} disabled={fishes === 0}>Eat a fish</button>
      <button onClick={addBoth}>Add both</button>
    </main>
  )
}

const container = document.createElement('div')
container.style.height = '400px'
document.body.appendChild(container)
createRoot(container).render(<App />)

The initial counts and total are zero. Click Add both to see one bear, one fish, and a total of two. Click Eat a fish to see the fish count fall to zero while the bear count stays at one. The button disables at zero; the store action itself does not enforce that limit.

3. Apply middleware to the combined store

Replace store.ts with this version to persist the counts using the shipped persist middleware. Keep the slice creators and main.tsx unchanged.

ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import {
  createBearSlice,
  createFishSlice,
  createSharedSlice,
  type BoundState,
} from './slices'

export const useBoundStore = create<BoundState>()(
  persist(
    (...args) => ({
      ...createBearSlice(...args),
      ...createFishSlice(...args),
      ...createSharedSlice(...args),
    }),
    {
      name: 'bear-fish-slices',
      partialize: (state) => ({ bears: state.bears, fishes: state.fishes }),
    },
  ),
)

In the browser, with localStorage available, change the counts and reload to see them restored. partialize saves only the two counts; the slice creators supply the actions again when the store is created.

Persistence options

The storage adapter uses PersistStorage; createJSONStorage supplies JSON serialization for a storage engine.

OptionTypeDefaultWhat it does
namestringRequiredSets the unique storage key for this combined store.
storagePersistStorage<PersistedState, PersistReturn> | undefinedcreateJSONStorage(() => window.localStorage)Selects the storage adapter.
partialize(state: S) => PersistedState(state) => stateFilters the state before saving it.

For storage and hydration configuration, see Persist store data.

Pitfalls

  • Keep slice creators unwrapped and put middleware around the combined creator, as above. See Type stores and middleware for middleware composition rules. When using devtools, annotate each slice's incoming mutator tuple as [['zustand/devtools', never]] instead of [].
  • Keep get() calls inside actions such as addBoth() and getBoth(). For initialization-time reads, see Reset store state.
  • Keep slice field names distinct. Object spreading makes a later slice's value overwrite an earlier value with the same key.

Was this page helpful?