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