title | nav |
---|---|
How to Migrate to v5 from v4 |
30 |
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.
- 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
- Other small improvements (technically breaking changes)
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,
})),
)
// ...
}
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'
- 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
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)