createWithEqualityFn react
createWithEqualityFn 让你可以创建一个附带 API 工具的 React Hook,就像 create 一样。
不过,它提供了一种定义自定义相等性检查的方式。这让你能更精细地控制组件何时重新渲染,
从而提升性能和响应速度。
WARNING
要使用 zustand/traditional 中的 createWithEqualityFn,你需要安装
use-sync-external-store 库,因为 zustand/traditional 依赖 useSyncExternalStoreWithSelector。
const useSomeStore = createWithEqualityFn(stateCreatorFn, defaultEqualityFn)
类型
签名
createWithEqualityFn<T>()(stateCreatorFn: StateCreator<T, [], []>, defaultEqualityFn?: <U>(a: U, b: U) => boolean): UseBoundStore<StoreApi<T>>
参考
createWithEqualityFn(stateCreatorFn, defaultEqualityFn)
参数
stateCreatorFn:一个接收 set 函数、get 函数和 store 作为参数的函数。
通常,你会返回一个对象,其中包含你想要暴露的方法。
- 可选
defaultEqualityFn:默认为 Object.is。一个让你可以跳过重新渲染的函数。
返回值
createWithEqualityFn 返回一个附带 API 工具的 React Hook,就像 create 一样。
它让你可以使用选择器函数返回基于当前状态的数据,并使用相等性函数跳过重新渲染。
它应该接收一个选择器函数和一个相等性函数作为参数。
用法
基于前一个状态更新状态
要基于前一个状态更新状态,我们应该使用更新函数(updater functions)。更多内容见这里。
这个示例展示了如何在 action 中支持更新函数。
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
type AgeStoreState = { age: number }
type AgeStoreActions = {
setAge: (
nextAge: AgeStoreState['age'] | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']),
) => void
}
type AgeStore = AgeStoreState & AgeStoreActions
const useAgeStore = createWithEqualityFn<AgeStore>()(
(set) => ({
age: 42,
setAge: (nextAge) =>
set((state) => ({
age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge,
})),
}),
shallow,
)
export default function App() {
const age = useAgeStore((state) => state.age)
const setAge = useAgeStore((state) => state.setAge)
function increment() {
setAge((currentAge) => currentAge + 1)
}
return (
<>
<h1>Your age: {age}</h1>
<button
type="button"
onClick={() => {
increment()
increment()
increment()
}}
>
+3
</button>
<button
type="button"
onClick={() => {
increment()
}}
>
+1
</button>
</>
)
}
更新状态中的原始值
状态可以保存任意类型的 JavaScript 值。当你想要更新数字、字符串、布尔值等内置原始值时,
应该直接赋新值,以确保更新被正确应用,并避免意外行为。
NOTE
默认情况下,set 函数执行的是浅合并。如果你需要用一个全新的状态完全替换现有状态,
请将 replace 参数设为 true。
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
type XStore = number
const useXStore = createWithEqualityFn<XStore>()(() => 0, shallow)
export default function MovingDot() {
const x = useXStore()
const setX = (nextX: number) => {
useXStore.setState(nextX, true)
}
const position = { y: 0, x }
return (
<div
onPointerMove={(e) => {
setX(e.clientX)
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}
>
<div
style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}}
/>
</div>
)
}
更新状态中的对象
对象在 JavaScript 中是可变的,但把它们存进状态时,你应该把它们当作不可变的来对待。
相反,当你想要更新一个对象时,需要创建一个新对象(或者复制一份现有对象),然后让状态使用这个新对象。
默认情况下,set 函数执行的是浅合并。对于大多数只需要修改特定属性的更新来说,默认的浅合并是更合适的选择,因为它更高效。
要完全用新状态替换现有状态,请谨慎使用 replace 参数并将其设为 true,因为这会丢弃状态中已有的任何嵌套数据。
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const usePositionStore = createWithEqualityFn<PositionStore>()(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
shallow,
)
export default function MovingDot() {
const position = usePositionStore((state) => state.position)
const setPosition = usePositionStore((state) => state.setPosition)
return (
<div
onPointerMove={(e) => {
setPosition({
x: e.clientX,
y: e.clientY,
})
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}
>
<div
style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}}
/>
</div>
)
}
更新状态中的数组
数组在 JavaScript 中是可变的,但把它们存进状态时,你应该把它们当作不可变的来对待。
和对象一样,当你想要更新状态中保存的数组时,需要创建一个新数组(或者复制一份现有数组),然后让状态使用这个新数组。
默认情况下,set 函数执行的是浅合并。要更新数组中的值,我们应该赋新值,以确保更新被正确应用,并避免意外行为。
要完全用新状态替换现有状态,请将 replace 参数设为 true。
WARNING
我们应该优先使用不可变操作,例如:[...array]、concat(...)、filter(...)、
slice(...)、map(...)、toSpliced(...)、toSorted(...) 和 toReversed(...),
并避免使用 array[arrayIndex] = ...、push(...)、unshift(...)、pop(...)、
shift(...)、splice(...)、reverse(...) 和 sort(...) 这类可变操作。
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
type PositionStore = [number, number]
const usePositionStore = createWithEqualityFn<PositionStore>()(() => [0, 0], shallow)
export default function MovingDot() {
const [x, y] = usePositionStore()
const position = { x, y }
const setPosition: typeof usePositionStore.setState = (nextPosition) => {
usePositionStore.setState(nextPosition, true)
}
return (
<div
onPointerMove={(e) => {
setPosition([e.clientX, e.clientY])
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}
>
<div
style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}}
/>
</div>
)
}
在不使用 store action 的情况下更新状态
在模块层级、store 外部定义 action 有几个好处,例如:不需要通过 Hook 就能调用 action,而且便于代码分割。
NOTE
推荐的做法是把 action 和状态放在 store 内部(让 action 与状态位于同一处)。
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
const usePositionStore = createWithEqualityFn<{
x: number
y: number
}>()(() => ({ x: 0, y: 0 }), shallow)
const setPosition: typeof usePositionStore.setState = (nextPosition) => {
usePositionStore.setState(nextPosition)
}
export default function MovingDot() {
const position = usePositionStore()
return (
<div
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}
>
<div
style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}}
onMouseEnter={(event) => {
const parent = event.currentTarget.parentElement
const parentWidth = parent.clientWidth
const parentHeight = parent.clientHeight
setPosition({
x: Math.ceil(Math.random() * parentWidth),
y: Math.ceil(Math.random() * parentHeight),
})
}}
/>
</div>
)
}
订阅状态更新
通过订阅状态更新,你注册一个回调,每当 store 的状态更新时它就会触发。我们可以用 subscribe 进行外部状态管理。
import { useEffect } from 'react'
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const usePositionStore = createWithEqualityFn<PositionStore>()(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (nextPosition) => set({ position: nextPosition }),
}),
shallow,
)
export default function MovingDot() {
const position = usePositionStore((state) => state.position)
const setPosition = usePositionStore((state) => state.setPosition)
useEffect(() => {
const unsubscribePositionStore = usePositionStore.subscribe(({ position }) => {
console.log('new position', { position })
})
return () => {
unsubscribePositionStore()
}
}, [])
return (
<div
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}
>
<div
style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}}
onMouseEnter={(event) => {
const parent = event.currentTarget.parentElement
const parentWidth = parent.clientWidth
const parentHeight = parent.clientHeight
setPosition({
x: Math.ceil(Math.random() * parentWidth),
y: Math.ceil(Math.random() * parentHeight),
})
}}
/>
</div>
)
}
疑难解答
我更新了状态,但屏幕没有更新
在上一个示例中,position 对象总是根据当前光标位置重新创建。但通常,你会希望把已有数据也纳入正在创建的新对象中。
例如,你可能只想更新表单中的一个字段,同时保留其他所有字段的原有值。
下面这些输入框无法正常工作,因为 onChange 处理函数直接修改了状态:
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
type PersonStoreState = {
person: { firstName: string; lastName: string; email: string }
}
type PersonStoreActions = {
setPerson: (nextPerson: PersonStoreState['person']) => void
}
type PersonStore = PersonStoreState & PersonStoreActions
const usePersonStore = createWithEqualityFn<PersonStore>()(
(set) => ({
person: {
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]',
},
setPerson: (person) => set({ person }),
}),
shallow,
)
export default function Form() {
const person = usePersonStore((state) => state.person)
const setPerson = usePersonStore((state) => state.setPerson)
function handleFirstNameChange(e: ChangeEvent<HTMLInputElement>) {
person.firstName = e.target.value
}
function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) {
person.lastName = e.target.value
}
function handleEmailChange(e: ChangeEvent<HTMLInputElement>) {
person.email = e.target.value
}
return (
<>
<label style={{ display: 'block' }}>
First name:
<input value={person.firstName} onChange={handleFirstNameChange} />
</label>
<label style={{ display: 'block' }}>
Last name:
<input value={person.lastName} onChange={handleLastNameChange} />
</label>
<label style={{ display: 'block' }}>
Email:
<input value={person.email} onChange={handleEmailChange} />
</label>
<p>
{person.firstName} {person.lastName} ({person.email})
</p>
</>
)
}
例如,下面这一行修改的是某次过去渲染中的状态:
person.firstName = e.target.value
要得到你想要的行为,可靠的做法是创建一个新对象并把它传给 setPerson。
但这里你还需要把已有数据复制进去,因为只有一个字段发生了变化:
setPerson({ ...person, firstName: e.target.value }) // New first name from the input
NOTE
由于 set 函数默认执行浅合并,我们不需要逐个复制每个属性。
现在表单可以正常工作了!
注意,你并没有为每个输入框单独声明一个状态变量。对于大型表单来说,把所有数据集中在一个对象里非常方便——前提是你正确地更新它!
import { type ChangeEvent } from 'react'
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
type PersonStoreState = {
person: { firstName: string; lastName: string; email: string }
}
type PersonStoreActions = {
setPerson: (nextPerson: PersonStoreState['person']) => void
}
type PersonStore = PersonStoreState & PersonStoreActions
const usePersonStore = createWithEqualityFn<PersonStore>()(
(set) => ({
person: {
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]',
},
setPerson: (nextPerson) => set({ person: nextPerson }),
}),
shallow,
)
export default function Form() {
const person = usePersonStore((state) => state.person)
const setPerson = usePersonStore((state) => state.setPerson)
function handleFirstNameChange(e: ChangeEvent<HTMLInputElement>) {
setPerson({ ...person, firstName: e.target.value })
}
function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) {
setPerson({ ...person, lastName: e.target.value })
}
function handleEmailChange(e: ChangeEvent<HTMLInputElement>) {
setPerson({ ...person, email: e.target.value })
}
return (
<>
<label style={{ display: 'block' }}>
First name:
<input value={person.firstName} onChange={handleFirstNameChange} />
</label>
<label style={{ display: 'block' }}>
Last name:
<input value={person.lastName} onChange={handleLastNameChange} />
</label>
<label style={{ display: 'block' }}>
Email:
<input value={person.email} onChange={handleEmailChange} />
</label>
<p>
{person.firstName} {person.lastName} ({person.email})
</p>
</>
)
}