How to Migrate to v5 from v4
We highly recommend to update to the latest version of v4, before migrating to v5. It will show all deprecation warnings without breaking your app.
Changes in v5
- Drop default exports
- Drop deprecated features
- Make React 18 the minimum required version
- Make use-sync-external-store a peer dependency (required for
createWithEqualityFn
anduseStoreWithEqualityFn
inzustand/traditional
) - Make TypeScript 4.5 the minimum required version
- Drop UMD/SystemJS support
- Organize entry points in the package.json
- Drop ES5 support
- Stricter types when setState's replace flag is set
- Persist middleware behavioral change
- Other small improvements (technically breaking changes)
Migration Guide
Using custom equality functions such as shallow
The create
function in v5 does not support customizing equality function.
If you use custom equality function such as shallow
,
the easiest migration is to use createWithEqualityFn
.
// v4
import { create } from 'zustand'
import { shallow } from 'zustand/shallow'
const useCountStore = create((set) => ({
count: 0,
text: 'hello',
// ...
}))
const Component = () => {
const { count, text } = useCountStore(
(state) => ({
count: state.count,
text: state.text,
}),
shallow,
)
// ...
}
That can be done with createWithEqualityFn
in v5:
npm install use-sync-external-store
// v5
import { createWithEqualityFn as create } from 'zustand/traditional'
// The rest is the same as v4
Alternatively, for the shallow
use case, you can use useShallow
hook:
// v5
import { create } from 'zustand'
import { useShallow } from 'zustand/shallow'
const useCountStore = create((set) => ({
count: 0,
text: 'hello',
// ...
}))
const Component = () => {
const { count, text } = useCountStore(
useShallow((state) => ({
count: state.count,
text: state.text,
})),
)
// ...
}
Requiring stable selector outputs
There is a behavioral change in v5 to match React default behavior. If a selector returns a new reference, it may cause infinite loops.
For example, this may cause infinite loops.
// v4
const [searchValue, setSearchValue] = useStore((state) => [
state.searchValue,
state.setSearchValue,
])
The error message will be something like this:
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
To fix it, use the useShallow
hook, which will return a stable reference.
// v5
import { useShallow } from 'zustand/shallow'
const [searchValue, setSearchValue] = useStore(
useShallow((state) => [state.searchValue, state.setSearchValue]),
)
Here's another example that may cause infinite loops.
// v4
const action = useMainStore((state) => {
return state.action ?? () => {}
})
To fix it, make sure the selector function returns a stable reference.
// v5
const FALLBACK_ACTION = () => {}
const action = useMainStore((state) => {
return state.action ?? FALLBACK_ACTION
})
Alternatively, if you need v4 behavior, createWithEqualityFn
will do.
// v5
import { createWithEqualityFn as create } from 'zustand/traditional'
Stricter types when setState's replace flag is set (Typescript only)
- setState:
- (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: boolean | undefined) => void;
+ setState:
+ (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false) => void;
+ (state: T | ((state: T) => T), replace: true) => void;
If you are not using the replace
flag, no migration is required.
If you are using the replace
flag and it's set to true
, you must provide a complete state object.
This change ensures that store.setState({}, true)
(which results in an invalid state) is no longer considered valid.
Examples:
// Partial state update (valid)
store.setState({ key: 'value' })
// Complete state replacement (valid)
store.setState({ key: 'value' }, true)
// Incomplete state replacement (invalid)
store.setState({}, true) // Error
Handling Dynamic replace
Flag
If the value of the replace
flag is dynamic and determined at runtime, you might face issues. To handle this, you can use a workaround by annotating the replace
parameter with as any
:
const replaceFlag = Math.random() > 0.5
store.setState(partialOrFull, replaceFlag as any)
Persist middlware no longer stores item at store creation
Previously, the persist
middleware stored the initial state during store creation. This behavior has been removed in v5 (and v4.5.5).
For example, in the following code, the initial state is stored in the storage.
// v4
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useCountStore = create(
persist(
() => ({
count: Math.floor(Math.random() * 1000),
}),
{
name: 'count',
},
),
)
In v5, this is no longer the case, and you need to explicitly set the state after store creation.
// v5
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useCountStore = create(
persist(
() => ({
count: 0,
}),
{
name: 'count',
},
),
)
useCountStore.setState({
count: Math.floor(Math.random() * 1000),
})