• 简体中文
  • 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>
        </>
      )
    }