| | |

Zustand 10 ⚛️ Slice Pattern — Splitting large stores into modular slices

⏱️ Estimated read: 14 min | |

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

ElementCode
Importimport type { StateCreator } from 'zustand'
Interfaceinterface Slice { ... }
Functionconst createSlice: StateCreator<Slice> = (set) => ({ ... })
Cross-sliceStateCreator<FullState, [], [], Slice>

Root Store Composition

MethodCode
Spreadcreate<FullState>()((...args) => ({ ...sliceA(...args), ...sliceB(...args) }))
Combinecreate(combine(initialState, (set) => ({ ... })))

TypeScript Types

TypePurpose
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

PitfallWhy It HappensFix
Circular importSlice imports the root storeType the slice with StateCreator
get().bears is undefinedSlice typed with only its own stateUse the full state type
Middleware type errorSlice type missing the middleware arrayAdd the middleware type parameter
Slice state is undefinedSlice function not spread into the root storeAdd ...createSlice(...args)
Root store type is wrongMissing a slice in the intersectionInclude 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

ItemValue
Slice definitionStateCreator<Slice> function
Cross-slice typeStateCreator<FullState, [], [], Slice>
Middleware typeSecond StateCreator parameter
Root compositionSpread the slice functions
Alternativecombine helper
Root store typeIntersection of all slice types
Circular dependencyAvoided by typing, not importing
Component consumptionOne hook, one store

Key takeaways:

  • A slice is a function, not an object. It receives set, get, and store and 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 as StateCreator<Slice> can only read its own state. A slice typed as StateCreator<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. The create<StoreState>() call types the store with the full state, and the selectors are typed automatically.
  • Middleware types must reach each slice. The second StateCreator parameter is an array of middleware tuples. A slice typed without the middleware cannot be combined with a store that has it, because set and get have different signatures .
  • The spread operator combines slices. create<FullState>()((...args) => ({ ...sliceA(...args), ...sliceB(...args) })). The ...args passes the set, get, and store arguments 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.


Was this helpful?


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!