Zustand 10 ⚛️ Slice Pattern — Splitting large stores into modular slices
A single store that holds every piece of application state becomes difficult to maintain as the application grows. The file grows to hundreds of lines, the interface lists unrelated fields, and a change to one feature requires editing the same file that another feature depends on. The slice pattern splits the store into smaller modules, each defining one feature’s state and actions, and combines them into a single store. The components still import one hook and one store; the split is in the definition, not in the consumption .
The previous chapters covered selectors, nested updates, and middleware. The slice pattern is the organizational structure that ties those pieces together. Each slice is a state creator function that receives set, get, and the store’s API, and returns a partial state. The root store combines the slices with the spread operator, or with combine from Zustand, and the resulting state is the union of all slices. The slices can read each other’s state through get(), which allows cross-slice actions without coupling the slices’ definitions.
Key point: A slice is a function, not an object. It receives set, get, and store as arguments and returns the slice’s state and actions. The root store calls each slice function and spreads the results into a single state object. TypeScript requires each slice to be typed as a StateCreator that describes only its own portion, and the root store’s type is the intersection of all slice types. Slices that need to read another slice’s state use get() with the full store type, not the slice’s partial type.
Why the slice pattern requires typed state creators
The monolithic-store problem.
A store with ten features has an interface with ten sets of fields and actions. Every component that imports the store’s type sees all ten, even if it uses only one. The file that defines the store is the single point of merge conflict when two developers change different features. The slice pattern reduces the interface to the feature’s own fields and actions, and the file to the feature’s own definition. The root store file is a short composition that imports the slices and combines them.
The circular-dependency problem.
A slice that reads another slice’s state needs access to the full store’s type. If the slice imports the root store to get the type, and the root store imports the slice to combine it, the import is circular. The solution is to type the slice’s get parameter with the full store type but not import the store itself. The type is defined in a separate file, or the slice is typed generically and the root store supplies the concrete type . The StateCreator type from Zustand accepts the full state type, the middleware type, and the slice’s own type as parameters, which allows the slice to be defined without importing the root store.
The middleware-order problem.
When the root store uses middleware, each slice must be typed with the same middleware. The StateCreator type has a second parameter for the middleware’s mutator, and the slice’s type must include it. A slice typed without the middleware cannot be combined with a store that has it, because the set and get functions have different signatures. The middleware type is passed through the slice’s StateCreator type, and the root store’s create call applies the middleware to the combined state creator .
a. Defining a slice
A slice is a function typed as StateCreator<FullState, [], [], SliceState>. The first type parameter is the full store type, the second is the middleware array, and the third is the slice’s own type.
import type { StateCreator } from 'zustand'
// The slice's own state and actions
export interface BearSlice {
bears: number
addBear: () => void
eatFish: () => void
}
// The slice function receives set and get
// The first type parameter is the slice's own type
export const createBearSlice: StateCreator<BearSlice> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ bears: state.bears - 1 })),
})
The slice function returns an object with the state and actions. The set call inside the slice is typed against the slice’s own state, so state.bears is recognized. The slice does not know about the other slices, and it does not import the root store.
When the slice needs to read another slice’s state, the first type parameter is the full store type, and the slice’s own type is the fourth parameter.
import type { StateCreator } from 'zustand'
export interface FishSlice {
fishes: number
addFish: () => void
}
// The full store type is the intersection of all slices
export type SharedState = BearSlice & FishSlice
// The slice can read the full state through get()
export const createFishSlice: StateCreator<SharedState, [], [], FishSlice> = (set, get) => ({
fishes: 0,
addFish: () => {
// Read the bear count from the full store
const bears = get().bears
set({ fishes: get().fishes + 1 })
console.log(`Bears: ${bears}`)
},
})
The StateCreator<SharedState, [], [], FishSlice> type tells TypeScript that the slice’s get returns the full store, so get().bears is valid even though bears is defined in the bear slice. The slice’s own type is the fourth parameter, so the returned object must match FishSlice .
b. Combining slices into the root store
The root store imports each slice function and spreads the results into a single state object. The root store’s type is the intersection of all slice types.
import { create } from 'zustand'
import { createBearSlice, type BearSlice } from './bearSlice'
import { createFishSlice, type FishSlice } from './fishSlice'
// The root store type is the union of all slice types
type StoreState = BearSlice & FishSlice
export const useBoundStore = create<StoreState>()((...args) => ({
...createBearSlice(...args),
...createFishSlice(...args),
}))
The ...args spread passes the set, get, and store arguments to each slice function. The spread operator combines the returned objects into one state object. The create<StoreState>() call types the store as the intersection of all slices, so useBoundStore((state) => state.bears) and useBoundStore((state) => state.fishes) are both valid .
The combine helper from Zustand is an alternative to the spread. It accepts the slices and a function that merges them, and it handles the TypeScript inference automatically.
import { create } from 'zustand'
import { combine } from 'zustand/middleware'
export const useBoundStore = create(
combine(
{ bears: 0, fishes: 0 }, // Initial state from all slices
(set) => ({
addBear: () => set((state) => ({ bears: state.bears + 1 })),
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
)
)
The combine helper merges the initial state and the actions into one object and infers the type. It is simpler than the spread for stores without middleware, but it is less flexible when slices need to read each other’s state or when middleware is involved .
c. Slices with middleware
When the root store uses middleware, each slice’s StateCreator type must include the middleware’s mutator in the second type parameter. The middleware type is defined by the middleware itself, and the slice must be typed with the same mutator.
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
import type { StateCreator } from 'zustand'
// The middleware mutator type
type Middleware = [['zustand/devtools', never], ['zustand/persist', unknown], ['zustand/immer', never]]
export interface BearSlice {
bears: number
addBear: () => void
}
// The slice is typed with the middleware array
export const createBearSlice: StateCreator<
BearSlice,
Middleware,
[],
BearSlice
> = (set) => ({
bears: 0,
addBear: () =>
set((state) => {
state.bears += 1
}, false, 'bear/add'),
})
// The root store applies the middleware
export const useBoundStore = create<BearSlice>()(
devtools(
persist(
immer((...args) => ({
...createBearSlice(...args),
})),
{ name: 'bear-storage' }
)
)
)
The Middleware type is an array of tuples, one per middleware, with the middleware’s name and its configuration type. The StateCreator type accepts this array as its second parameter, so the slice’s set and get are typed with the middleware’s behavior. The root store applies the middleware to the combined state creator, and the slices are called inside the middleware’s scope .
The middleware type is verbose, and it must be updated when the middleware chain changes. A common pattern is to define a helper type that lists the middleware once and reuse it across all slices. The helper type is the single source of truth for the middleware chain, and the slices import it instead of declaring the array inline.
Complete Example Session
// ============================================
// PART 1: THE BEAR SLICE
// ============================================
import type { StateCreator } from 'zustand'
export interface BearSlice {
bears: number
addBear: () => void
removeBear: () => void
}
export const createBearSlice: StateCreator<BearSlice> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
removeBear: () => set((state) => ({ bears: state.bears - 1 })),
})
// ============================================
// PART 2: THE FISH SLICE
// ============================================
export interface FishSlice {
fishes: number
addFish: () => void
}
export const createFishSlice: StateCreator<FishSlice> = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
// ============================================
// PART 3: A CROSS-SLICE ACTION
// ============================================
export interface SharedSlice {
total: () => number
}
// The full state type is the intersection of the other slices
type FullState = BearSlice & FishSlice & SharedSlice
export const createSharedSlice: StateCreator<
FullState,
[],
[],
SharedSlice
> = (_set, get) => ({
// Reads state from both slices
total: () => get().bears + get().fishes,
})
// ============================================
// PART 4: THE ROOT STORE
// ============================================
import { create } from 'zustand'
type StoreState = BearSlice & FishSlice & SharedSlice
export const useBoundStore = create<StoreState>()((...args) => ({
...createBearSlice(...args),
...createFishSlice(...args),
...createSharedSlice(...args),
}))
// ============================================
// PART 5: THE COMPONENT THAT USES ONE SLICE
// ============================================
function BearCounter() {
const bears = useBoundStore((state) => state.bears)
const addBear = useBoundStore((state) => state.addBear)
return (
<div>
<p>Bears: {bears}</p>
<button onClick={addBear}>Add bear</button>
</div>
)
}
// ============================================
// PART 6: THE COMPONENT THAT USES THE CROSS-SLICE ACTION
// ============================================
function TotalDisplay() {
const total = useBoundStore((state) => state.total())
return <p>Total animals: {total}</p>
}
The six parts demonstrate the complete slice pattern: the bear slice, the fish slice, the cross-slice shared slice, the root store that combines them, and the components that consume individual slices. The critical distinction is between Parts 1 and 3: the bear slice is typed with only its own state, while the shared slice is typed with the full state so that it can read both slices.
Quick Reference
Slice Definition
| Element | Code |
|---|---|
| Import | import type { StateCreator } from 'zustand' |
| Interface | interface Slice { ... } |
| Function | const createSlice: StateCreator<Slice> = (set) => ({ ... }) |
| Cross-slice | StateCreator<FullState, [], [], Slice> |
Root Store Composition
| Method | Code |
|---|---|
| Spread | create<FullState>()((...args) => ({ ...sliceA(...args), ...sliceB(...args) })) |
| Combine | create(combine(initialState, (set) => ({ ... }))) |
TypeScript Types
| Type | Purpose |
|---|---|
StateCreator<Slice> | Slice with only its own state |
StateCreator<Full, [], [], Slice> | Slice that reads the full state |
StateCreator<Slice, Middleware, [], Slice> | Slice with middleware |
Best Practices
✅ Do This:
// Define each slice in its own file
// bearSlice.ts, fishSlice.ts, sharedSlice.ts # ✅
// Type each slice with StateCreator
const createBearSlice: StateCreator<BearSlice> = (set) => ({ ... }) # ✅
// Type cross-slice slices with the full state
const createSharedSlice: StateCreator<FullState, [], [], SharedSlice> = ... # ✅
// Combine slices with the spread operator
create<FullState>()((...args) => ({ ...sliceA(...args), ...sliceB(...args) })) # ✅
// Type slices with middleware when the root store uses it
StateCreator<Slice, [['zustand/immer', never]], [], Slice> # ✅
// Keep the root store file small
// It imports the slices and combines them # ✅
❌ Don’t Do This:
// Put all features in one store file
// The file grows to hundreds of lines # ❌
// Import the root store inside a slice
import { useBoundStore } from './store' // Circular dependency # ❌
// Type a cross-slice slice with only its own state
StateCreator<SharedSlice> // Cannot read get().bears # ❌
// Forget the middleware type parameter
StateCreator<Slice> // Incompatible with the middleware's set # ❌
// Use the slice function as an object
useBoundStore(createBearSlice) // It is a function, not a hook # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Circular import | Slice imports the root store | Type the slice with StateCreator |
get().bears is undefined | Slice typed with only its own state | Use the full state type |
| Middleware type error | Slice type missing the middleware array | Add the middleware type parameter |
Slice state is undefined | Slice function not spread into the root store | Add ...createSlice(...args) |
| Root store type is wrong | Missing a slice in the intersection | Include all slice types |
Real-World Examples
1. Bear slice
const createBearSlice: StateCreator<BearSlice> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
})
2. Fish slice
const createFishSlice: StateCreator<FishSlice> = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
3. Cross-slice action
const createSharedSlice: StateCreator<FullState, [], [], SharedSlice> = (_set, get) => ({
total: () => get().bears + get().fishes,
})
4. Root store with spread
create<FullState>()((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args) }))
5. Root store with combine
create(combine({ bears: 0 }, (set) => ({ addBear: () => set((s) => ({ bears: s.bears + 1 })) })))
6. Slice with Immer
const createSlice: StateCreator<Slice, [['zustand/immer', never]], [], Slice> = (set) => ({
items: [],
add: (item) => set((state) => { state.items.push(item) }),
})
7. Slice with persist
const createSlice: StateCreator<Slice, [['zustand/persist', unknown]], [], Slice> = (set) => ({ ... })
8. Component using one slice
const bears = useBoundStore((state) => state.bears)
9. Component using a cross-slice action
const total = useBoundStore((state) => state.total())
10. Middleware type helper
type Middleware = [['zustand/devtools', never], ['zustand/persist', unknown], ['zustand/immer', never]]
Visual
Slice Composition
┌──────────────────────────────────────────────────────────────┐
│ SLICES ARE COMBINED INTO ONE STORE │
│ │
│ bearSlice.ts fishSlice.ts │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ bears: 0 │ │ fishes: 0 │ │
│ │ addBear: fn │ │ addFish: fn │ │
│ └─────────┬──────────┘ └─────────┬──────────┘ │
│ │ │ │
│ └────────────┬────────────┘ │
│ │ │
│ ▼ │
│ store.ts │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ create<FullState>()((...args) => ({ │ │
│ │ ...createBearSlice(...args), │ │
│ │ ...createFishSlice(...args), │ │
│ │ })) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Store: { bears, addBear, fishes, addFish } │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Components import one hook and select the fields they need. │
└──────────────────────────────────────────────────────────────┘
Cross-Slice Access
┌──────────────────────────────────────────────────────────────┐
│ A SLICE READS ANOTHER SLICE'S STATE THROUGH get() │
│ │
│ Bear slice (typed with only BearSlice): │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ StateCreator<BearSlice> │ │
│ │ get() returns { bears, addBear, removeBear } │ │
│ │ Cannot see fishes │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Shared slice (typed with FullState): │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ StateCreator<FullState, [], [], SharedSlice> │ │
│ │ get() returns { bears, fishes, total, ... } │ │
│ │ Can see both bears and fishes │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ The type parameter determines what get() can see. │
└──────────────────────────────────────────────────────────────┘
Middleware Type Flow
┌──────────────────────────────────────────────────────────────┐
│ THE MIDDLEWARE TYPE MUST REACH EACH SLICE │
│ │
│ Root store: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ devtools(persist(immer((...args) => ({ │ │
│ │ ...createBearSlice(...args), │ │
│ │ })))) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Slice type: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ StateCreator< │ │
│ │ BearSlice, │ │
│ │ [['zustand/devtools', never], │ │
│ │ ['zustand/persist', unknown], │ │
│ │ ['zustand/immer', never]], │ │
│ │ [], │ │
│ │ BearSlice │ │
│ │ > │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Without the middleware array, set() and get() have the │
│ wrong signature and the slice cannot be combined. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Slice definition | StateCreator<Slice> function |
| Cross-slice type | StateCreator<FullState, [], [], Slice> |
| Middleware type | Second StateCreator parameter |
| Root composition | Spread the slice functions |
| Alternative | combine helper |
| Root store type | Intersection of all slice types |
| Circular dependency | Avoided by typing, not importing |
| Component consumption | One hook, one store |
Key takeaways:
- A slice is a function, not an object. It receives
set,get, andstoreand returns the slice’s state and actions. The root store calls each slice function and spreads the results into one state object . - The slice’s type determines what
get()can see. A slice typed asStateCreator<Slice>can only read its own state. A slice typed asStateCreator<FullState, [], [], Slice>can read the entire store, which is necessary for cross-slice actions. - The root store’s type is the intersection of all slices.
type StoreState = BearSlice & FishSlice & SharedSlice. Thecreate<StoreState>()call types the store with the full state, and the selectors are typed automatically. - Middleware types must reach each slice. The second
StateCreatorparameter is an array of middleware tuples. A slice typed without the middleware cannot be combined with a store that has it, becausesetandgethave different signatures . - The spread operator combines slices.
create<FullState>()((...args) => ({ ...sliceA(...args), ...sliceB(...args) })). The...argspasses theset,get, andstorearguments to each slice function. - The root store file is a short composition. The slice files hold the feature logic, and the root store imports and combines them. A change to one feature touches one file, and the merge conflicts are limited to that file.
Remember: The slice pattern is the organizational structure for a growing Zustand store. Each feature lives in its own slice, the slice is typed with StateCreator, and the root store combines the slices with the spread operator. Cross-slice actions read the full state through get(), and the type parameters determine what each slice can see. The middleware chain is applied to the root store, and the middleware type is threaded through each slice. The result is a store that scales in features without scaling in complexity: one file per feature, one root composition, and one hook for the components.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!