Skip to main content
The only breaking changes in v4 are in types. If you are using Zustand with TypeScript or JSDoc type annotations, this guide applies. Otherwise, no migration is required.
It’s recommended to first read the TypeScript Guide to make the migration easier to understand. You can also check the diff of test files in the Zustand repository from v3 to v4.

create

Applicable imports:
import create from 'zustand'
import create from 'zustand/vanilla'
Type signature change:
- create:
-   < State
-   , StoreSetState = StoreApi<State>["set"]
-   , StoreGetState = StoreApi<State>["get"]
-   , Store = StoreApi<State>
-   >
-     (f: ...) => ...
+ create:
+   { <State>(): (f: ...) => ...
+   , <State, Mutators>(f: ...) => ...
+   }
1

No type parameters

If you are not passing any type parameters to create, no migration is required.
2

Using leaf middleware (combine or redux)

If you are using a “leaf” middleware like combine or redux, remove all type parameters from create.
3

Other cases

Replace create<T, ...>(...) with create<T>()(...) (note the double parentheses).

StateCreator

Applicable imports:
import type { StateCreator } from 'zustand'
import type { StateCreator } from 'zustand/vanilla'
Type signature change:
- type StateCreator
-   < State
-   , StoreSetState = StoreApi<State>["set"]
-   , StoreGetState = StoreApi<State>["get"]
-   , Store = StoreApi<State>
-   > =
-     ...
+ type StateCreator
+   < State
+   , InMutators extends [StoreMutatorIdentifier, unknown][] = []
+   , OutMutators extends [StoreMutatorIdentifier, unknown][] = []
+   , Return = State
+   > =
+     ...
If you are using StateCreator, you are likely authoring a middleware or using the “slices” pattern. Check the TypeScript Guide for more information on authoring middlewares and the slices pattern.

PartialState

Applicable imports:
import type { PartialState } from 'zustand'
import type { PartialState } from 'zustand/vanilla'
Type signature change:
- type PartialState
-   < T extends State
-   , K1 extends keyof T = keyof T
-   , K2 extends keyof T = K1
-   , K3 extends keyof T = K2
-   , K4 extends keyof T = K3
-   > =
-   | (Pick<T, K1> | Pick<T, K2> | Pick<T, K3> | Pick<T, K4> | T)
-   | ((state: T) => Pick<T, K1> | Pick<T, K2> | Pick<T, K3> | Pick<T, K4> | T)
+ type PartialState<T> =
+   | Partial<T>
+   | ((state: T) => Partial<T>)
1

Replace type parameters

Replace PartialState<T, ...> with PartialState<T>
2

Enable exactOptionalPropertyTypes

Enable exactOptionalPropertyTypes in your tsconfig.json:
{
  "compilerOptions": {
    "exactOptionalPropertyTypes": true
  }
}
We’re no longer using the trick to disallow { foo: undefined } to be assigned to Partial<{ foo: string }>. Instead, we’re relying on the exactOptionalPropertyTypes TypeScript option.

useStore

Applicable imports:
import { useStore } from 'zustand'
import { useStore } from 'zustand/react'
Type signature change:
- useStore:
-   { <State>(store: StoreApi<State>): State
-   , <State, StateSlice>
-       ( store: StoreApi<State>
-       , selector: StateSelector<State, StateSlice>,
-       , equals?: EqualityChecker<StateSlice>
-       ): StateSlice
-   }
+ useStore:
+   <Store, StateSlice = ExtractState<Store>>
+     ( store: Store
+     , selector?: StateSelector<State, StateSlice>,
+     , equals?: EqualityChecker<StateSlice>
+     )
+       => StateSlice
1

No type parameters

If you are not passing any type parameters to useStore, no migration is required.
2

With type parameters

Remove all type parameters, or pass the store type instead of the state type as the first parameter.

UseBoundStore

Applicable imports:
import type { UseBoundStore } from 'zustand'
import type { UseBoundStore } from 'zustand/react'
Type signature change:
- type UseBoundStore<
-   State,
-   Store = StoreApi<State>
- > =
-   & { (): T
-     , <StateSlice>
-         ( selector: StateSelector<State, StateSlice>
-         , equals?: EqualityChecker<StateSlice>
-         ): U
-     }
-   & Store
+ type UseBoundStore<Store> =
+   & (<StateSlice = ExtractState<S>>
+       ( selector?: (state: ExtractState<S>) => StateSlice
+       , equals?: (a: StateSlice, b: StateSlice) => boolean
+       ) => StateSlice
+     )
+   & S
Migration: Replace UseBoundStore<T> with UseBoundStore<StoreApi<T>>, and UseBoundStore<T, S> with UseBoundStore<S>.

UseContextStore

Applicable imports:
import type { UseContextStore } from 'zustand/context'
Change:
- type UseContextStore
The UseContextStore type has been removed. Use typeof MyContext.useStore instead.

createContext

Applicable imports:
import createContext from 'zustand/context'
Type signature change:
  createContext:
-   <State, Store = StoreApi<State>>() => ...
+   <Store>() => ...
Migration: Replace createContext<T>() with createContext<StoreApi<T>>(), and createContext<T, S>() with createContext<S>().

Middleware: combine, devtools, subscribeWithSelector

Applicable imports:
import { combine } from 'zustand/middleware'
import { devtools } from 'zustand/middleware'
import { subscribeWithSelector } from 'zustand/middleware'
Type signature changes:
- combine:
-   <T, U>(...) => ...
+ combine:
+   <T, U, Mps, Mcs>(...) => ...

- devtools:
-   <T>(...) => ...
+ devtools:
+   <T, Mps, Mcs>(...) => ...

- subscribeWithSelector:
-   <T>(...) => ...
+ subscribeWithSelector:
+   <T, Mps, Mcs>(...) => ...
If you are not passing any type parameters to combine, devtools, or subscribeWithSelector, no migration is required.If you are, remove all the type parameters, as they are inferred automatically.

Middleware: persist

Applicable imports:
import { persist } from 'zustand/middleware'
Type signature change:
- persist:
-   <T, U = Partial<T>>(...) => ...
+ persist:
+   <T, Mps, Mcs, U = T>(...) => ...
1

Remove type parameters

If you are passing any type parameters, remove them as they are inferred automatically.
2

Check if you're using partialize option

If you are passing the partialize option, no further steps are required for migration.
3

Handle compilation errors (if any)

The type of partialized state is now T instead of Partial<T>, which aligns with the runtime behavior of the default partialize, which is an identity (s => s).
If you see compilation errors, you have to find and fix the errors yourself, as they might indicate unsound code.Workaround: Pass s => s as Partial<typeof s> to partialize. If your partialized state is truly Partial<T>, you should not encounter any bugs.
The runtime behavior has not changed, only the types are now correct.

Middleware: redux

Applicable imports:
import { redux } from 'zustand/middleware'
Type signature change:
- redux:
-   <T, A>(...) => ...
+ redux:
+   <T, A, Mps, Mcs>(...) => ...
Migration: If you are not passing any type parameters to redux, no migration is required. If you are, remove all the type parameters and annotate only the second (action) parameter:
- redux<T, A>((state, action) => ..., ...)
+ redux((state, action: A) => ..., ...)

Build docs developers (and LLMs) love