The combine middleware simplifies store creation by automatically inferring types from your initial state and actions. This eliminates the need for explicit type definitions and makes the curried version of create unnecessary when using middleware.
import { combine } from 'zustand/middleware'
const useStore = create (
combine (
{ count: 0 }, // Initial state
( set ) => ({ // Actions
increment : () => set (( state ) => ({ count: state . count + 1 })),
})
)
)
Signature
combine < T , U >(
initialState : T ,
additionalStateCreator : StateCreator < T , [], [], U >
): StateCreator < Omit < T , keyof U > & U , [], [] >
Parameters
The initial state object. Can be any type except a function.
additionalStateCreator
StateCreator<T, [], [], U>
required
A function that takes set, get, and store as arguments, returning an object with actions and computed values.
Basic Usage
No explicit types needed - they’re inferred automatically:
import { create } from 'zustand'
import { combine } from 'zustand/middleware'
const useCounterStore = create (
combine (
{ count: 0 },
( set ) => ({
increment : () => set (( state ) => ({ count: state . count + 1 })),
decrement : () => set (( state ) => ({ count: state . count - 1 })),
reset : () => set ({ count: 0 }),
})
)
)
// TypeScript knows the exact shape
function Counter () {
const count = useCounterStore (( state ) => state . count ) // number
const increment = useCounterStore (( state ) => state . increment ) // () => void
return (
< div >
< p > Count : { count }</ p >
< button onClick = { increment } > Increment </ button >
</ div >
)
}
Without Combine
Traditional approach requires explicit types:
import { create } from 'zustand'
type Store = {
count : number
increment : () => void
decrement : () => void
}
const useStore = create < Store >()(( set ) => ({
count: 0 ,
increment : () => set (( state ) => ({ count: state . count + 1 })),
decrement : () => set (( state ) => ({ count: state . count - 1 })),
}))
With Combine
Types are automatically inferred:
import { create } from 'zustand'
import { combine } from 'zustand/middleware'
const useStore = create (
combine (
{ count: 0 },
( set ) => ({
increment : () => set (( state ) => ({ count: state . count + 1 })),
decrement : () => set (( state ) => ({ count: state . count - 1 })),
})
)
)
// All types are inferred, no manual type definitions needed!
Complex State
const useUserStore = create (
combine (
{
user: null as { name : string ; email : string } | null ,
loading: false ,
error: null as string | null ,
},
( set ) => ({
setUser : ( user : { name : string ; email : string }) =>
set ({ user , loading: false , error: null }),
setLoading : ( loading : boolean ) => set ({ loading }),
setError : ( error : string ) => set ({ error , loading: false }),
logout : () => set ({ user: null , error: null }),
})
)
)
Using get for Derived Logic
const useCartStore = create (
combine (
{
items: [] as Array <{ id : string ; price : number ; quantity : number }>,
},
( set , get ) => ({
addItem : ( item : { id : string ; price : number ; quantity : number }) =>
set ({ items: [ ... get (). items , item ] }),
removeItem : ( id : string ) =>
set ({ items: get (). items . filter (( item ) => item . id !== id ) }),
getTotal : () =>
get (). items . reduce (( sum , item ) => sum + item . price * item . quantity , 0 ),
clear : () => set ({ items: [] }),
})
)
)
Vanilla Store
import { createStore } from 'zustand/vanilla'
import { combine } from 'zustand/middleware'
const positionStore = createStore (
combine (
{ position: { x: 0 , y: 0 } },
( set ) => ({
setPosition : ( position : { x : number ; y : number }) => set ({ position }),
})
)
)
const dotContainer = document . getElementById ( 'dot-container' ) as HTMLDivElement
const dot = document . getElementById ( 'dot' ) as HTMLDivElement
dotContainer . addEventListener ( 'pointermove' , ( event ) => {
positionStore . getState (). setPosition ({
x: event . clientX ,
y: event . clientY ,
})
})
const render = ( state : ReturnType < typeof positionStore . getState >) => {
dot . style . transform = `translate( ${ state . position . x } px, ${ state . position . y } px)`
}
render ( positionStore . getInitialState ())
positionStore . subscribe ( render )
Composing with Middleware
import { devtools , persist } from 'zustand/middleware'
import { combine } from 'zustand/middleware'
const useStore = create (
devtools (
persist (
combine (
{ count: 0 },
( set ) => ({
increment : () =>
set (( state ) => ({ count: state . count + 1 }), undefined , 'increment' ),
decrement : () =>
set (( state ) => ({ count: state . count - 1 }), undefined , 'decrement' ),
})
),
{ name: 'counter-storage' }
),
{ name: 'CounterStore' }
)
)
Multiple State Slices
const useStore = create (
combine (
{
user: { name: 'John' , email: '[email protected] ' },
settings: { theme: 'light' as const , notifications: true },
ui: { sidebarOpen: false , modalOpen: false },
},
( set ) => ({
updateUser : ( updates : Partial <{ name : string ; email : string }>) =>
set (( state ) => ({ user: { ... state . user , ... updates } })),
updateSettings : ( updates : Partial <{ theme : 'light' | 'dark' ; notifications : boolean }>) =>
set (( state ) => ({ settings: { ... state . settings , ... updates } })),
toggleSidebar : () =>
set (( state ) => ({ ui: { ... state . ui , sidebarOpen: ! state . ui . sidebarOpen } })),
openModal : () =>
set (( state ) => ({ ui: { ... state . ui , modalOpen: true } })),
closeModal : () =>
set (( state ) => ({ ui: { ... state . ui , modalOpen: false } })),
})
)
)
Async Actions
const useDataStore = create (
combine (
{
data: null as string [] | null ,
loading: false ,
error: null as string | null ,
},
( set ) => ({
fetchData : async () => {
set ({ loading: true , error: null })
try {
const response = await fetch ( '/api/data' )
const data = await response . json ()
set ({ data , loading: false })
} catch ( error ) {
set ({ error: error . message , loading: false })
}
},
})
)
)
Benefits
Type Inference Automatically infers types from initial state and actions, reducing boilerplate.
Simpler Syntax No need for curried create()() syntax when using middleware.
Better DX Autocomplete works perfectly without manual type definitions.
Less Errors Reduced chance of type mismatches between state and actions.
When to Use
Use combine when:
You want automatic type inference
You’re using middleware and want cleaner syntax
Your state structure is straightforward
You prefer separating state from actions
Avoid combine when:
You need complex type transformations
You have circular type dependencies
You prefer all state and actions together