We highly recommend updating 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 and useStoreWithEqualityFn in zustand/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.
Before (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,
)
// ...
}
After (v5):
Install use-sync-external-store
npm install use-sync-external-store
Update imports to use createWithEqualityFn
import { createWithEqualityFn as create } from 'zustand/traditional'
// The rest is the same as v4
Alternative approach using useShallow hook:
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:
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.
Use the useShallow hook
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: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 can use this workaround:
const replaceFlag = Math.random() > 0.5
const args = [{ bears: 5 }, replaceFlag] as Parameters<
typeof useBearStore.setState
>
store.setState(...args)
Persist middleware 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, 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),
})
Additional Resources