How to Combine Multiple Stores or Use the `combine` Middleware in Zustand
Zustand provides two robust patterns for composing state: the combine middleware for automatic type inference and the slice pattern for modular architecture, both ultimately producing a single StateCreator passed to create or createStore.
The pmndrs/zustand library offers flexible ways to structure complex application state without sacrificing type safety or performance. Whether you need to merge independent state slices into one unified hook or leverage the combine middleware to infer TypeScript types automatically, understanding these composition patterns is essential for scalable state management.
Using the combine Middleware
The combine middleware, located in src/middleware/combine.ts, merges an initial state object with a state-creator function that adds actions. It returns a new StateCreator that spreads the initial object alongside the generated state, automatically inferring the final type as Write<T, U>—where T represents the initial state and U represents the slice.
This approach eliminates the need to manually write interfaces because TypeScript derives the complete store shape from the arguments passed to combine.
// src/store/positionStore.ts
import { createStore } from 'zustand/vanilla'
import { combine } from 'zustand/middleware'
export const positionStore = createStore(
combine(
// Initial state
{ position: { x: 0, y: 0 } },
// Slice adding actions
(set) => ({
setPosition: (pos: { x: number; y: number }) => set({ position: pos })
})
)
)
When using vanilla stores (outside React), combine works seamlessly with createStore to produce a store instance you can subscribe to directly.
The Slice Pattern for Modular Stores
The slice pattern treats each feature module as an independent function that receives the standard (set, get, api) arguments and returns its own slice of state and actions. According to docs/learn/guides/slices-pattern.md, the final store assembles these slices by spreading their results inside a single create call.
This pattern excels when you want clear separation of concerns, with each slice living in its own file and potentially being imported selectively.
// src/slices/fishSlice.ts
export const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((s) => ({ fishes: s.fishes + 1 })),
})
// src/slices/bearSlice.ts
export const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((s) => ({ bears: s.bears + 1 })),
eatFish: () => set((s) => ({ fishes: s.fishes - 1 })),
})
// src/store/useBoundStore.ts
import { create } from 'zustand'
import { createBearSlice } from './slices/bearSlice'
import { createFishSlice } from './slices/fishSlice'
export const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}))
The rest parameters ...a distribute the set, get, and api arguments to each slice, ensuring every slice operates on the same underlying store instance.
Adding Middleware to Combined Stores
Both composition methods support wrapping with additional middleware such as persist or devtools. As noted in docs/reference/middlewares/combine.md, you should apply middleware once around the entire combined creator rather than inside individual slices to avoid duplicate state handling or conflicting middleware instances.
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { createBearSlice } from './slices/bearSlice'
import { createFishSlice } from './slices/fishSlice'
export const useBoundStore = create(
persist(
(...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}),
{ name: 'bound-storage' }
)
)
This pattern works because both combine and the slice pattern ultimately return a StateCreator function compatible with create and createStore from src/vanilla.ts and src/react.ts.
Summary
combinemiddleware: Located insrc/middleware/combine.ts, merges initial state with action creators and automatically infers TypeScript types viaWrite<T, U>.- Slice pattern: Documented in
docs/learn/guides/slices-pattern.md, creates modular state slices that are spread into a singlecreatecall for clean separation of concerns. - Middleware compatibility: Wrap the final combined creator with
persist,devtools, orimmerexactly once to avoid the "middleware inside a slice" anti-pattern. - Underlying mechanism: Both approaches produce a
StateCreatorconsumed bycreate(React) orcreateStore(vanilla) functions.
Frequently Asked Questions
What is the difference between combine and the slice pattern?
The combine middleware explicitly merges an initial state object with a function returning additional state and actions, offering automatic type inference without manual interfaces. The slice pattern emphasizes architectural separation, where each feature exports a factory function that receives (set, get, api) and returns its state slice, promoting code colocation and selective imports. Both achieve state composition but optimize for different developer workflows.
Can I use combine with TypeScript?
Yes, combine is designed for TypeScript. It infers the complete store type as Write<T, U>, merging the initial state T with the creator's return type U. This eliminates the need to manually declare interfaces for the combined state, as documented in docs/reference/middlewares/combine.md.
How do I add persistence to a combined Zustand store?
Apply the persist middleware once around the entire combined creator. Whether using combine or the slice pattern, wrap the final StateCreator with persist(...) before passing it to create. This ensures the middleware captures the complete state tree rather than individual slices.
Is the slice pattern better for large applications?
The slice pattern scales well for large codebases because it enforces modular boundaries—each slice lives in its own file and manages a specific domain. However, combine offers superior ergonomics for smaller stores or when type inference is prioritized. Large applications often adopt the slice pattern for organizational clarity while still leveraging combine within individual slices if automatic typing is desired.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →