• 简体中文
  • useStoreWithEqualityFn react

    useStoreWithEqualityFn 是一个 React Hook,让你可以像 useStore 一样在 React 中使用原生 store。不过,它提供了一种定义自定义相等性检查的方式。这让你能更细致地控制组件何时重新渲染,从而提升性能和响应速度。

    WARNING

    要使用 zustand/traditional 中的 useStoreWithEqualityFn,你需要安装 use-sync-external-store 库,因为 zustand/traditional 依赖 useSyncExternalStoreWithSelector。

    const someState = useStoreWithEqualityFn(store, selectorFn, equalityFn)

    类型

    签名

    useStoreWithEqualityFn<T, U = T>(store: StoreApi<T>, selectorFn: (state: T) => U, equalityFn?: (a: U, b: U) => boolean): U

    参考

    useStoreWithEqualityFn(store, selectorFn, equalityFn)

    参数

    • storeApi:可让你访问 store API 工具的实例。
    • selectorFn:一个用于返回基于当前状态的数据的函数。
    • equalityFn:一个用于跳过重新渲染的函数。

    返回值

    useStoreWithEqualityFn 会根据选择器函数返回基于当前状态的任意数据,并让你通过相等性函数跳过重新渲染。它接收一个 store、一个选择器函数和一个相等性函数作为参数。

    用法

    在 React 中使用全局原生 store

    首先,我们创建一个 store 来保存点在屏幕上的位置。这个 store 将管理 x 和 y 坐标,并提供一个用于更新这些坐标的 action。

    import { createStore, useStore } from 'zustand'
    
    type PositionStoreState = { position: { x: number; y: number } }
    
    type PositionStoreActions = {
      setPosition: (nextPosition: PositionStoreState['position']) => void
    }
    
    type PositionStore = PositionStoreState & PositionStoreActions
    
    const positionStore = createStore<PositionStore>()((set) => ({
      position: { x: 0, y: 0 },
      setPosition: (position) => set({ position }),
    }))

    接下来,我们创建一个 MovingDot 组件,它渲染一个表示点的 div。该组件将使用这个 store 来跟踪并更新点的位置。

    function MovingDot() {
      const position = useStoreWithEqualityFn(positionStore, (state) => state.position, shallow)
      const setPosition = useStoreWithEqualityFn(positionStore, (state) => state.setPosition, shallow)
    
      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>
      )
    }

    最后,我们在 App 组件中渲染 MovingDot 组件。

    export default function App() {
      return <MovingDot />
    }

    完整代码应该如下所示:

    import { createStore } from 'zustand'
    import { useStoreWithEqualityFn } from 'zustand/traditional'
    import { shallow } from 'zustand/shallow'
    
    type PositionStoreState = { position: { x: number; y: number } }
    
    type PositionStoreActions = {
      setPosition: (nextPosition: PositionStoreState['position']) => void
    }
    
    type PositionStore = PositionStoreState & PositionStoreActions
    
    const positionStore = createStore<PositionStore>()((set) => ({
      position: { x: 0, y: 0 },
      setPosition: (position) => set({ position }),
    }))
    
    function MovingDot() {
      const position = useStoreWithEqualityFn(positionStore, (state) => state.position, shallow)
      const setPosition = useStoreWithEqualityFn(positionStore, (state) => state.setPosition, shallow)
    
      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>
      )
    }
    
    export default function App() {
      return <MovingDot />
    }

    在 React 中使用动态全局原生 store

    首先,我们创建一个工厂函数,用于生成管理计数器状态的 store。每个标签页都会拥有这个 store 的独立实例。

    import { createStore } from 'zustand'
    
    type CounterState = {
      count: number
    }
    
    type CounterActions = { increment: () => void }
    
    type CounterStore = CounterState & CounterActions
    
    const createCounterStore = () => {
      return createStore<CounterStore>()((set) => ({
        count: 0,
        increment: () => {
          set((state) => ({ count: state.count + 1 }))
        },
      }))
    }

    接下来,我们创建一个工厂函数,用于管理计数器 store 的创建与获取。这样每个标签页都能拥有各自独立的计数器。

    const defaultCounterStores = new Map<string, ReturnType<typeof createCounterStore>>()
    
    const createCounterStoreFactory = (counterStores: typeof defaultCounterStores) => {
      return (counterStoreKey: string) => {
        if (!counterStores.has(counterStoreKey)) {
          counterStores.set(counterStoreKey, createCounterStore())
        }
        return counterStores.get(counterStoreKey)!
      }
    }
    
    const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores)

    现在,我们来构建 Tabs 组件,用户可以在其中切换标签页,并分别递增每个标签页的计数器。

    const [currentTabIndex, setCurrentTabIndex] = useState(0)
    const counterState = useStoreWithEqualityFn(
      getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`),
      (state) => state,
      shallow,
    )
    
    return (
      <div style={{ fontFamily: 'monospace' }}>
        <div
          style={{
            display: 'flex',
            gap: '0.5rem',
            borderBottom: '1px solid salmon',
            paddingBottom: 4,
          }}
        >
          <button
            type="button"
            style={{
              border: '1px solid salmon',
              backgroundColor: '#fff',
              cursor: 'pointer',
            }}
            onClick={() => setCurrentTabIndex(0)}
          >
            Tab 1
          </button>
          <button
            type="button"
            style={{
              border: '1px solid salmon',
              backgroundColor: '#fff',
              cursor: 'pointer',
            }}
            onClick={() => setCurrentTabIndex(1)}
          >
            Tab 2
          </button>
          <button
            type="button"
            style={{
              border: '1px solid salmon',
              backgroundColor: '#fff',
              cursor: 'pointer',
            }}
            onClick={() => setCurrentTabIndex(2)}
          >
            Tab 3
          </button>
        </div>
        <div style={{ padding: 4 }}>
          Content of Tab {currentTabIndex + 1}
          <br /> <br />
          <button type="button" onClick={() => counterState.increment()}>
            Count: {counterState.count}
          </button>
        </div>
      </div>
    )

    最后,我们创建 App 组件,它渲染各个标签页及其对应的计数器。每个标签页的计数器状态都是独立管理的。

    export default function App() {
      return <Tabs />
    }

    完整代码应该如下所示:

    import { useState } from 'react'
    import { createStore } from 'zustand'
    import { useStoreWithEqualityFn } from 'zustand/traditional'
    import { shallow } from 'zustand/shallow'
    
    type CounterState = {
      count: number
    }
    
    type CounterActions = { increment: () => void }
    
    type CounterStore = CounterState & CounterActions
    
    const createCounterStore = () => {
      return createStore<CounterStore>()((set) => ({
        count: 0,
        increment: () => {
          set((state) => ({ count: state.count + 1 }))
        },
      }))
    }
    
    const defaultCounterStores = new Map<string, ReturnType<typeof createCounterStore>>()
    
    const createCounterStoreFactory = (counterStores: typeof defaultCounterStores) => {
      return (counterStoreKey: string) => {
        if (!counterStores.has(counterStoreKey)) {
          counterStores.set(counterStoreKey, createCounterStore())
        }
        return counterStores.get(counterStoreKey)!
      }
    }
    
    const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores)
    
    export default function App() {
      const [currentTabIndex, setCurrentTabIndex] = useState(0)
      const counterState = useStoreWithEqualityFn(
        getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`),
        (state) => state,
        shallow,
      )
    
      return (
        <div style={{ fontFamily: 'monospace' }}>
          <div
            style={{
              display: 'flex',
              gap: '0.5rem',
              borderBottom: '1px solid salmon',
              paddingBottom: 4,
            }}
          >
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(0)}
            >
              Tab 1
            </button>
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(1)}
            >
              Tab 2
            </button>
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(2)}
            >
              Tab 3
            </button>
          </div>
          <div style={{ padding: 4 }}>
            Content of Tab {currentTabIndex + 1}
            <br /> <br />
            <button type="button" onClick={() => counterState.increment()}>
              Count: {counterState.count}
            </button>
          </div>
        </div>
      )
    }

    在 React 中使用作用域内(非全局)原生 store

    首先,我们创建一个 store 来保存点在屏幕上的位置。这个 store 将管理 x 和 y 坐标,并提供一个用于更新这些坐标的 action。

    type PositionStoreState = { position: { x: number; y: number } }
    
    type PositionStoreActions = {
      setPosition: (nextPosition: PositionStoreState['position']) => void
    }
    
    type PositionStore = PositionStoreState & PositionStoreActions
    
    const createPositionStore = () => {
      return createStore<PositionStore>()((set) => ({
        position: { x: 0, y: 0 },
        setPosition: (position) => set({ position }),
      }))
    }

    接下来,我们创建一个上下文和一个 Provider 组件,让 store 能够沿着 React 组件树向下传递。这样每个 MovingDot 组件都能拥有各自独立的状态。

    const PositionStoreContext = createContext<ReturnType<typeof createPositionStore> | null>(null)
    
    function PositionStoreProvider({ children }: { children: ReactNode }) {
      const [store] = useState(() => createPositionStore())
      return <PositionStoreContext.Provider value={store}>{children}</PositionStoreContext.Provider>
    }

    为了简化对 store 的访问,我们创建一个名为 usePositionStore 的 React 自定义 Hook。这个 Hook 会从上下文中读取 store,并让我们选择状态中的特定部分。

    function usePositionStore<U>(selector: (state: PositionStore) => U) {
      const store = useContext(PositionStoreContext)
    
      if (store === null) {
        throw new Error('usePositionStore must be used within PositionStoreProvider')
      }
    
      return useStoreWithEqualityFn(store, selector, shallow)
    }

    现在,我们创建 MovingDot 组件,它会渲染一个在容器内跟随鼠标光标的点。

    function MovingDot({ color }: { color: string }) {
      const position = usePositionStore((state) => state.position)
      const setPosition = usePositionStore((state) => state.setPosition)
    
      return (
        <div
          onPointerMove={(e) => {
            setPosition({
              x:
                e.clientX > e.currentTarget.clientWidth
                  ? e.clientX - e.currentTarget.clientWidth
                  : e.clientX,
              y: e.clientY,
            })
          }}
          style={{
            position: 'relative',
            width: '50vw',
            height: '100vh',
          }}
        >
          <div
            style={{
              position: 'absolute',
              backgroundColor: color,
              borderRadius: '50%',
              transform: `translate(${position.x}px, ${position.y}px)`,
              left: -10,
              top: -10,
              width: 20,
              height: 20,
            }}
          />
        </div>
      )
    }

    最后,我们在 App 组件中把所有内容组合起来:渲染两个 MovingDot 组件,每个组件都拥有各自独立的状态。

    export default function App() {
      return (
        <div style={{ display: 'flex' }}>
          <PositionStoreProvider>
            <MovingDot color="red" />
          </PositionStoreProvider>
          <PositionStoreProvider>
            <MovingDot color="blue" />
          </PositionStoreProvider>
        </div>
      )
    }

    完整代码应该如下所示:

    import { type ReactNode, useState, createContext, useContext } from 'react'
    import { createStore } from 'zustand'
    import { useStoreWithEqualityFn } from 'zustand/traditional'
    import { shallow } from 'zustand/shallow'
    
    type PositionStoreState = { position: { x: number; y: number } }
    
    type PositionStoreActions = {
      setPosition: (nextPosition: PositionStoreState['position']) => void
    }
    
    type PositionStore = PositionStoreState & PositionStoreActions
    
    const createPositionStore = () => {
      return createStore<PositionStore>()((set) => ({
        position: { x: 0, y: 0 },
        setPosition: (position) => set({ position }),
      }))
    }
    
    const PositionStoreContext = createContext<ReturnType<typeof createPositionStore> | null>(null)
    
    function PositionStoreProvider({ children }: { children: ReactNode }) {
      const [store] = useState(() => createPositionStore())
      return <PositionStoreContext.Provider value={store}>{children}</PositionStoreContext.Provider>
    }
    
    function usePositionStore<U>(selector: (state: PositionStore) => U) {
      const store = useContext(PositionStoreContext)
    
      if (store === null) {
        throw new Error('usePositionStore must be used within PositionStoreProvider')
      }
    
      return useStoreWithEqualityFn(store, selector, shallow)
    }
    
    function MovingDot({ color }: { color: string }) {
      const position = usePositionStore((state) => state.position)
      const setPosition = usePositionStore((state) => state.setPosition)
    
      return (
        <div
          onPointerMove={(e) => {
            setPosition({
              x:
                e.clientX > e.currentTarget.clientWidth
                  ? e.clientX - e.currentTarget.clientWidth
                  : e.clientX,
              y: e.clientY,
            })
          }}
          style={{
            position: 'relative',
            width: '50vw',
            height: '100vh',
          }}
        >
          <div
            style={{
              position: 'absolute',
              backgroundColor: color,
              borderRadius: '50%',
              transform: `translate(${position.x}px, ${position.y}px)`,
              left: -10,
              top: -10,
              width: 20,
              height: 20,
            }}
          />
        </div>
      )
    }
    
    export default function App() {
      return (
        <div style={{ display: 'flex' }}>
          <PositionStoreProvider>
            <MovingDot color="red" />
          </PositionStoreProvider>
          <PositionStoreProvider>
            <MovingDot color="blue" />
          </PositionStoreProvider>
        </div>
      )
    }

    在 React 中使用动态作用域内(非全局)原生 store

    首先,我们创建一个工厂函数,用于生成管理计数器状态的 store。每个标签页都会拥有这个 store 的独立实例。

    type CounterState = {
      count: number
    }
    
    type CounterActions = { increment: () => void }
    
    type CounterStore = CounterState & CounterActions
    
    const createCounterStore = () => {
      return createStore<CounterStore>()((set) => ({
        count: 0,
        increment: () => {
          set((state) => ({ count: state.count + 1 }))
        },
      }))
    }

    接下来,我们创建一个工厂函数,用于管理计数器 store 的创建与获取。这样每个标签页都能拥有各自独立的计数器。

    const createCounterStoreFactory = (
      counterStores: Map<string, ReturnType<typeof createCounterStore>>,
    ) => {
      return (counterStoreKey: string) => {
        if (!counterStores.has(counterStoreKey)) {
          counterStores.set(counterStoreKey, createCounterStore())
        }
        return counterStores.get(counterStoreKey)!
      }
    }

    接下来,我们需要一种在整个应用中管理和访问这些 store 的方式。为此,我们使用 React 的上下文。

    const CounterStoresContext = createContext(null)
    
    const CounterStoresProvider = ({ children }) => {
      const [stores] = useState(() => new Map<string, ReturnType<typeof createCounterStore>>())
    
      return <CounterStoresContext.Provider value={stores}>{children}</CounterStoresContext.Provider>
    }

    现在,我们创建一个自定义 Hook useCounterStore,用于访问给定标签页对应的正确 store。

    const useCounterStore = <U,>(key: string, selector: (state: CounterStore) => U) => {
      const stores = useContext(CounterStoresContext)
    
      if (stores === undefined) {
        throw new Error('useCounterStore must be used within CounterStoresProvider')
      }
    
      const getOrCreateCounterStoreByKey = useCallback(
        (key: string) => createCounterStoreFactory(stores!)(key),
        [stores],
      )
    
      return useStore(getOrCreateCounterStoreByKey(key), selector)
    }

    现在,我们来构建 Tabs 组件,用户可以在其中切换标签页,并分别递增每个标签页的计数器。

    function Tabs() {
      const [currentTabIndex, setCurrentTabIndex] = useState(0)
      const counterState = useCounterStore(`tab-${currentTabIndex}`, (state) => state)
    
      return (
        <div style={{ fontFamily: 'monospace' }}>
          <div
            style={{
              display: 'flex',
              gap: '0.5rem',
              borderBottom: '1px solid salmon',
              paddingBottom: 4,
            }}
          >
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(0)}
            >
              Tab 1
            </button>
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(1)}
            >
              Tab 2
            </button>
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(2)}
            >
              Tab 3
            </button>
          </div>
          <div style={{ padding: 4 }}>
            Content of Tab {currentTabIndex + 1}
            <br /> <br />
            <button type="button" onClick={() => counterState.increment()}>
              Count: {counterState.count}
            </button>
          </div>
        </div>
      )
    }

    最后,我们创建 App 组件,它渲染各个标签页及其对应的计数器。每个标签页的计数器状态都是独立管理的。

    export default function App() {
      return (
        <CounterStoresProvider>
          <Tabs />
        </CounterStoresProvider>
      )
    }

    完整代码应该如下所示:

    import { type ReactNode, useState, useCallback, useContext, createContext } from 'react'
    import { createStore, useStore } from 'zustand'
    
    type CounterState = {
      count: number
    }
    
    type CounterActions = { increment: () => void }
    
    type CounterStore = CounterState & CounterActions
    
    const createCounterStore = () => {
      return createStore<CounterStore>()((set) => ({
        count: 0,
        increment: () => {
          set((state) => ({ count: state.count + 1 }))
        },
      }))
    }
    
    const createCounterStoreFactory = (
      counterStores: Map<string, ReturnType<typeof createCounterStore>>,
    ) => {
      return (counterStoreKey: string) => {
        if (!counterStores.has(counterStoreKey)) {
          counterStores.set(counterStoreKey, createCounterStore())
        }
        return counterStores.get(counterStoreKey)!
      }
    }
    
    const CounterStoresContext = createContext<Map<
      string,
      ReturnType<typeof createCounterStore>
    > | null>(null)
    
    const CounterStoresProvider = ({ children }: { children: ReactNode }) => {
      const [stores] = useState(() => new Map<string, ReturnType<typeof createCounterStore>>())
    
      return <CounterStoresContext.Provider value={stores}>{children}</CounterStoresContext.Provider>
    }
    
    const useCounterStore = <U,>(key: string, selector: (state: CounterStore) => U) => {
      const stores = useContext(CounterStoresContext)
    
      if (stores === undefined) {
        throw new Error('useCounterStore must be used within CounterStoresProvider')
      }
    
      const getOrCreateCounterStoreByKey = useCallback(
        (key: string) => createCounterStoreFactory(stores!)(key),
        [stores],
      )
    
      return useStore(getOrCreateCounterStoreByKey(key), selector)
    }
    
    function Tabs() {
      const [currentTabIndex, setCurrentTabIndex] = useState(0)
      const counterState = useCounterStore(`tab-${currentTabIndex}`, (state) => state)
    
      return (
        <div style={{ fontFamily: 'monospace' }}>
          <div
            style={{
              display: 'flex',
              gap: '0.5rem',
              borderBottom: '1px solid salmon',
              paddingBottom: 4,
            }}
          >
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(0)}
            >
              Tab 1
            </button>
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(1)}
            >
              Tab 2
            </button>
            <button
              type="button"
              style={{
                border: '1px solid salmon',
                backgroundColor: '#fff',
                cursor: 'pointer',
              }}
              onClick={() => setCurrentTabIndex(2)}
            >
              Tab 3
            </button>
          </div>
          <div style={{ padding: 4 }}>
            Content of Tab {currentTabIndex + 1}
            <br /> <br />
            <button type="button" onClick={() => counterState.increment()}>
              Count: {counterState.count}
            </button>
          </div>
        </div>
      )
    }
    
    export default function App() {
      return (
        <CounterStoresProvider>
          <Tabs />
        </CounterStoresProvider>
      )
    }

    疑难解答

    TBD