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:
tsimport 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.
tsimport { 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:
tsximport { 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.
tsimport { 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.
| Option | Type | Default | What it does |
|---|---|---|---|
name | string | Required | Sets the unique storage key for this combined store. |
storage | PersistStorage<PersistedState, PersistReturn> | undefined | createJSONStorage(() => window.localStorage) | Selects the storage adapter. |
partialize | (state: S) => PersistedState | (state) => state | Filters 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 asaddBoth()andgetBoth(). 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.
Related
- How Zustand works — the single-store model and colocated actions.
- State and actions — updates and shallow merging.
- Selectors and subscriptions — selecting values from the combined hook.
Was this page helpful?