Migrating to v4

The only breaking changes are in types. If you are using Zustand with TypeScript or JSDoc type annotations, this guide applies. Otherwise, no migration is required.

Also, it's recommended to first read the new TypeScript Guide, so that the migration is easier to understand.

In addition to this migration guide, you can also check the diff of the test files in the Zustand repository from v3 to v4.

create

Applicable imports

import create from 'zustand'
import create from 'zustand/vanilla'

Change

- create:
-   < State
-   , StoreSetState = StoreApi<State>["set"]
-   , StoreGetState = StoreApi<State>["get"]
-   , Store = StoreApi<State>
-   >
-     (f: ...) => ...
+ create:
+   { <State>(): (f: ...) => ...
+   , <State, Mutators>(f: ...) => ...
+   }

Migration

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

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

Else, replace create<T, ...>(...) with create<T>()(...).

StateCreator

Applicable imports

import type { StateCreator } from 'zustand'
import type { StateCreator } from 'zustand/vanilla'

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
+   > =
+     ...

Migration

If you are using StateCreator, you are likely authoring a middleware or using the "slices" pattern. For that check the Authoring middlewares and advanced usage and Common recipes sections of the TypeScript Guide.

PartialState

Applicable imports

import type { PartialState } from 'zustand'
import type { PartialState } from 'zustand/vanilla'

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>)

Migration

Replace PartialState<T, ...> with PartialState<T> and preferably turn on 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 users to turn on exactOptionalPropertyTypes.

useStore

Applicable imports

import { useStore } from 'zustand'
import { useStore } from 'zustand/react'

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

Migration

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

If you are, it's recommended to remove all the 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'

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

Migration

Use typeof MyContext.useStore instead

createContext

Applicable imports

import createContext from 'zustand/context'

Change

  createContext:
-   <State, Store = StoreApi<State>>() => ...
+   <Store>() => ...

Migration

Replace createContext<T>() with createContext<StoreApi<T>>(), and createContext<T, S>() with createContext<S>().

combine, devtools, subscribeWithSelector

Applicable imports

import { combine } from 'zustand/middleware'
import { devtools } from 'zustand/middleware'
import { subscribeWithSelector } from 'zustand/middleware'

Change

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

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

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

Migration

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.

persist

Applicable imports

import { persist } from 'zustand/middleware'

Change

- persist:
-   <T, U = Partial<T>>(...) => ...
+ persist:
+   <T, Mps, Mcs, U = T>(...) => ...

Migration

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

Next, if you are passing the partialize option, there is no further steps required for migration.

If you are not passing the partialize option, you might see some compilation errors. If you do not see any, there is no further migration required.

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 some compilation errors, you have to find and fix the errors yourself, because they might be indicative of unsound code. Alternatively, the workaround will be passing 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.

redux

Applicable imports

import { redux } from 'zustand/middleware'

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. That is, replace redux<T, A>((state, action) => ..., ...) with redux((state, action: A) => ..., ...).