Skip to content

createWithEqualityFn ⚛️

createWithEqualityFn 允许你创建一个附加了 API 实用程序的 React Hook,就像 create 一样。但是,它提供了一种定义自定义相等性检查的方法。这允许更精细地控制组件何时重新渲染,从而提高性能和响应能力。

¥createWithEqualityFn lets you create a React Hook with API utilities attached, just like create. However, it offers a way to define a custom equality check. This allows for more granular control over when components re-render, improving performance and responsiveness.

js
const useSomeStore = createWithEqualityFn(stateCreatorFn, equalityFn)

类型

¥Types

签名

¥Signature

ts
createWithEqualityFn<T>()(stateCreatorFn: StateCreator<T, [], []>, equalityFn?: (a: T, b: T) => boolean): UseBoundStore<StoreApi<T>>

参考

¥Reference

createWithEqualityFn(stateCreatorFn)

参数

¥Parameters

  • stateCreatorFn:以 set 函数、get 函数和 store 作为参数的函数。通常,你将返回一个包含要公开的方法的对象。

    ¥stateCreatorFn: A function that takes set function, get function and store as arguments. Usually, you will return an object with the methods you want to expose.

  • 可选 equalityFn:默认为 Object.is。允许你跳过重新渲染的函数。

    ¥optional equalityFn: Defaults to Object.is. A function that lets you skip re-renders.

返回

¥Returns

createWithEqualityFn 返回一个附加了 API 实用程序的 React Hook,就像 create 一样。它允许你使用选择器函数返回基于当前状态的数据,并允许你使用相等函数跳过重新渲染。它应该将选择器函数和相等函数作为参数。

¥createWithEqualityFn returns a React Hook with API utilities attached, just like create. It lets you return data that is based on current state, using a selector function, and lets you skip re-renders using an equality function. It should take a selector function, and an equality function as arguments.

用法

¥Usage

根据先前状态更新状态

¥Updating state based on previous state

要根据先前的状态更新状态,我们应该使用更新程序函数。阅读有关 此处 的更多信息。

¥To update a state based on previous state we should use updater functions. Read more about that here.

此示例展示了如何在操作中支持更新程序功能。

¥This example shows how you can support updater functions within actions.

tsx
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>
    </>
  )
}

更新状态中的原始内容

¥Updating Primitives in State

状态可以保存任何类型的 JavaScript 值。当你想要更新内置原始值(如数字、字符串、布尔值等)时,你应该直接分配新值以确保正确应用更新,并避免意外行为。

¥State can hold any kind of JavaScript value. When you want to update built-in primitive values like numbers, strings, booleans, etc. you should directly assign new values to ensure updates are applied correctly, and avoid unexpected behaviors.

默认情况下,`set` 函数执行浅合并。如果你需要用新状态完全替换状态,请使用设置为 `true` 的 `replace` 参数

¥[!NOTE] By default, set function performs a shallow merge. If you need to completely replace the state with a new one, use the replace parameter set to true

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

更新状态中的对象

¥Updating Objects in State

对象在 JavaScript 中是可变的,但当你将它们存储在状态中时,你应该将它们视为不可变的。相反,当你想要更新对象时,你需要创建一个新对象(或复制现有对象),然后设置状态以使用新对象。

¥Objects are mutable in JavaScript, but you should treat them as immutable when you store them in state. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use the new object.

默认情况下,set 函数执行浅合并。对于大多数只需要修改特定属性的更新,默认浅合并是首选,因为它更有效率。要用新状态完全替换旧状态,请谨慎使用设置为 truereplace 参数,因为它会丢弃状态内任何现有的嵌套数据。

¥By default, set function performs a shallow merge. For most updates where you only need to modify specific properties, the default shallow merge is preferred as it's more efficient. To completely replace the state with a new one, use the replace parameter set to true with caution, as it discards any existing nested data within the state.

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

更新状态中的数组

¥Updating Arrays in State

数组在 JavaScript 中是可变的,但当你将它们存储在状态中时,你应该将它们视为不可变的。与对象一样,当你想要更新存储在状态中的数组时,你需要创建一个新数组(或复制现有数组),然后设置状态以使用新数组。

¥Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array.

默认情况下,set 函数执行浅合并。要更新数组值,我们应该分配新值以确保正确应用更新,并避免意外行为。要用新状态完全替换旧状态,请将 replace 参数设置为 true

¥By default, set function performs a shallow merge. To update array values we should assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely replace the state with a new one, use the replace parameter set to true.

我们应该更喜欢不可变的操作,例如:`[...array]`、`concat(...)`、`filter(...)`、`slice(...)`、`map(...)`、`toSpliced(...)`、`toSorted(...)` 和 `toReversed(...)`,并避免可变操作,如 `array[arrayIndex] = ...`、`push(...)`、`unshift(...)`、`pop(...)`、`shift(...)`、`splice(...)`、`reverse(...)` 和 `sort(...)`。

¥[!IMPORTANT] We should prefer immutable operations like: [...array], concat(...), filter(...), slice(...), map(...), toSpliced(...), toSorted(...), and toReversed(...), and avoid mutable operations like array[arrayIndex] = ..., push(...), unshift(...), pop(...), shift(...), splice(...), reverse(...), and sort(...).

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

不使用存储操作更新状态

¥Updating state with no store actions

在模块中定义操作级别,外部存储有几个优点,例如:它不需要钩子来调用操作,并且它有助于代码拆分。

¥Defining actions at module level, external to the store have a few advantages like: it doesn't require a hook to call an action, and it facilitates code splitting.

建议的方式是将操作和状态放在存储中(让你的操作与你的状态放在一起)。

¥[!NOTE] The recommended way is to colocate actions and states within the store (let your actions be located together with your state).

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

订阅状态更新

¥Subscribing to state updates

通过订阅状态更新,你可以注册一个回调,该回调在存储的状态更新时触发。我们可以使用 subscribe 进行外部状态管理。

¥By subscribing to state updates, you register a callback that fires whenever the store's state updates. We can use subscribe for external state management.

tsx
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(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>
  )
}

故障排除

¥Troubleshooting

我已经更新了状态,但屏幕没有更新

¥I’ve updated the state, but the screen doesn’t update

在前面的例子中,position 对象始终从当前光标位置重新创建。但是通常,你会希望将现有数据作为你正在创建的新对象的一部分。例如,你可能只想更新表单中的一个字段,但保留所有其他字段的先前值。

¥In the previous example, the position object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields.

这些输入字段不起作用,因为 onChange 处理程序会改变状态:

¥These input fields don’t work because the onChange handlers mutate the state:

tsx
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: 'bhepworth@sculpture.com',
    },
    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>
    </>
  )
}

例如,此行会从过去的渲染中改变状态:

¥For example, this line mutates the state from a past render:

tsx
person.firstName = e.target.value

获取所需行为的可靠方法是创建一个新对象并将其传递给 setPerson。但在这里你还想将现有数据复制到其中,因为只有一个字段发生了变化:

¥The reliable way to get the behavior you’re looking for is to create a new object and pass it to setPerson. But here you want to also copy the existing data into it because only one of the fields has changed:

ts
setPerson({ ...person, firstName: e.target.value }) // New first name from the input

由于 `set` 函数默认执行浅合并,因此我们不需要单独复制每个属性。

¥[!NOTE] We don’t need to copy every property separately due to set function performing shallow merge by default.

现在表单可以工作了!

¥Now the form works!

请注意你没有为每个输入字段声明单独的状态变量。对于大型表单,将所有数据分组在一个对象中非常方便 - 只要你正确更新它!

¥Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly!

tsx
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: 'bhepworth@sculpture.com',
    },
    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>
    </>
  )
}

Zustand v5.0 中文网 - 粤ICP备13048890号