Split a store into typed slices
Use slices when one store contains several feature areas. Each slice owns a part of the state and its actions, while the spread composition creates one store that components select from.
When to use this pattern
Split a growing store when its state and actions have clear feature boundaries but still need to work together. Keep the slices as state-creator functions; do not create a separate Zustand store for each slice. The application gets one hook, one state model, and cross-feature actions through get.
1. Define the slice state and actions
Start with the complete store type, then type each slice as a part of that store. The StateCreator generic receives the full store type and returns only the slice it creates. This lets a bear action update fish state and lets a shared action call actions from both slices.
tsexport type BearSlice = {
bears: number
addBear: () => void
eatFish: () => void
}
export type FishSlice = {
fishes: number
addFish: () => void
}
export type SharedSlice = {
addBearAndFish: () => void
}
export type Store = BearSlice & FishSlice & SharedSlice
tsimport type { StateCreator } from 'zustand'
import type { BearSlice, Store } from './types'
export const createBearSlice: StateCreator<Store, [], [], BearSlice> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
tsimport type { StateCreator } from 'zustand'
import type { FishSlice, Store } from './types'
export const createFishSlice: StateCreator<Store, [], [], FishSlice> = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
tsimport type { StateCreator } from 'zustand'
import type { SharedSlice, Store } from './types'
export const createSharedSlice: StateCreator<Store, [], [], SharedSlice> = (_set, get) => ({
addBearAndFish: () => {
get().addBear()
get().addFish()
},
})
The bear and fish actions update state through set. The shared action reads the current store through get, so it can reuse actions from other slices.
2. Compose one store
Call create once and spread every slice into its state creator; that single composition is the difference this pattern makes. For the general typed-store use of create, see Typed store patterns. The returned useBoundStore hook exposes all three slices as one state model.
tsimport { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { createBearSlice } from './bearSlice'
import { createFishSlice } from './fishSlice'
import { createSharedSlice } from './sharedSlice'
import type { Store } from './types'
export const useBoundStore = create<Store>()(
devtools(
persist(
(...args) => ({
...createBearSlice(...args),
...createFishSlice(...args),
...createSharedSlice(...args),
}),
{ name: 'bound-store' },
),
{ name: 'bound-store' },
),
)
Apply persist and devtools around the combined creator, not inside an individual slice. For their general roles and composition inside create, see Typed store patterns; the slice-specific rule is to keep middleware at the boundary of the complete store. persist stores the combined state under bound-store; devtools names the same store in Redux DevTools.
The important order is devtools(persist(...)): the repository recommends putting devtools as far outside the other middleware as possible because it changes setState.
3. Select slices in React
Select individual fields or actions from the single hook. The mounted component below initially shows 0 bears and 0 fish. Add a bear changes only the bear count; Add both changes both counts in one action.
tsximport { useBoundStore } from './stores/useBoundStore'
export function App() {
const bears = useBoundStore((state: ReturnType<typeof useBoundStore.getState>) => state.bears)
const fishes = useBoundStore((state: ReturnType<typeof useBoundStore.getState>) => state.fishes)
const addBear = useBoundStore((state: ReturnType<typeof useBoundStore.getState>) => state.addBear)
const addBearAndFish = useBoundStore((state: ReturnType<typeof useBoundStore.getState>) => state.addBearAndFish)
return (
<main>
<h1>Animals</h1>
<p>Bears: {bears}</p>
<p>Fish: {fishes}</p>
<button type="button" onClick={addBear}>Add a bear</button>
<button type="button" onClick={addBearAndFish}>Add both</button>
</main>
)
}
The selectors keep the component connected to the fields it reads while the actions remain colocated with their state. A slice can therefore grow without changing the component's store import or creating a provider.
Options that matter here
| Option | Type | Default | What it does |
|---|---|---|---|
name | string | Not specified | Names the persisted entry and the Redux DevTools connection in this example. |
Pitfalls
- Apply middleware only to the combined creator, not inside a slice; see Typed store patterns for the general middleware pattern.
- Do not call
getwhile the initial state creator is running. Use it from a cross-slice action after the store exists; see Typed store patterns for the general creator and initializer rules. - Select stable values from the store. A selector that creates a new reference on every render can cause an infinite update loop in v5; see Selectors and rendering.
Was this page helpful?