主题
useStoreWithEqualityFn ⚛️
useStoreWithEqualityFn
是一个 React Hook,可让你在 React 中使用原始存储,就像 useStore
一样。但是,它提供了一种定义自定义相等性检查的方法。这允许更精细地控制组件何时重新渲染,从而提高性能和响应能力。
¥useStoreWithEqualityFn
is a React Hook that lets you use a vanilla store in React, just like useStore
. 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 someState = useStoreWithEqualityFn(store, selectorFn, equalityFn)
签名
¥Signature
ts
useStoreWithEqualityFn<T, U = T>(store: StoreApi<T>, selectorFn: (state: T) => U, equalityFn?: (a: T, b: T) => boolean): U
参考
¥Reference
useStoreWithEqualityFn(store, selectorFn, equalityFn)
参数
¥Parameters
storeApi
:允许你访问存储 API 实用程序的实例。¥
storeApi
: The instance that lets you access to store API utilities.selectorFn
:允许你返回基于当前状态的数据的函数。¥
selectorFn
: A function that lets you return data that is based on current state.equalityFn
:允许你跳过重新渲染的函数。¥
equalityFn
: A function that lets you skip re-renders.
返回
¥Returns
useStoreWithEqualityFn
根据选择器函数返回基于当前状态的任何数据,并允许你使用相等函数跳过重新渲染。它应该将存储、选择器函数和相等函数作为参数。
¥useStoreWithEqualityFn
returns any data based on current state depending on the selector function, and lets you skip re-renders using an equality function. It should take a store, a selector function, and an equality function as arguments.
用法
¥Usage
在 React 中使用全局 vanilla 存储
¥Using a global vanilla store in React
首先,让我们设置一个存储来保存屏幕上点的位置。我们将定义存储来管理 x
和 y
坐标,并提供更新这些坐标的操作。
¥First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage x
and y
coordinates and provide an action to update these coordinates.
tsx
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。此组件将使用存储来跟踪和更新点的位置。
¥Next, we'll create a MovingDot
component that renders a div representing the dot. This component will use the store to track and update the dot's position.
tsx
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
组件。
¥Finally, we’ll render the MovingDot
component in our App
component.
tsx
export default function App() {
return <MovingDot />
}
代码应该是这样的:
¥Here is what the code should look like:
tsx
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 中使用动态全局 vanilla 存储
¥Using dynamic global vanilla stores in React
首先,我们将创建一个工厂函数,生成一个用于管理计数器状态的存储。每个选项卡都有自己的此存储实例。
¥First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store.
ts
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 }))
},
}))
}
接下来,我们将创建一个工厂函数来管理计数器存储的创建和检索。这允许每个选项卡拥有自己独立的计数器。
¥Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter.
ts
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 组件,用户可以在其中切换选项卡并增加每个选项卡的计数器。
¥Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter.
tsx
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
组件,它将渲染选项卡及其各自的计数器。每个选项卡的计数器状态都是独立管理的。
¥Finally, we'll create the App
component, which renders the tabs and their respective counters. The counter state is managed independently for each tab.
tsx
export default function App() {
return <Tabs />
}
代码应该是这样的:
¥Here is what the code should look like:
tsx
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 中使用作用域(非全局)原始存储
¥Using scoped (non-global) vanilla store in React
首先,让我们设置一个存储来保存屏幕上点的位置。我们将定义存储来管理 x
和 y
坐标,并提供更新这些坐标的操作。
¥First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage x
and y
coordinates and provide an action to update these coordinates.
tsx
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 }),
}))
}
接下来,我们将创建一个上下文和一个提供程序组件,以便通过 React 组件树向下传递存储。这允许每个 MovingDot
组件拥有自己独立的状态。
¥Next, we'll create a context and a provider component to pass down the store through the React component tree. This allows each MovingDot
component to have its own independent state.
tsx
const PositionStoreContext = createContext<ReturnType<
typeof createPositionStore
> | null>(null)
function PositionStoreProvider({ children }: { children: ReactNode }) {
const [positionStore] = useState(createPositionStore)
return (
<PositionStoreContext.Provider value={positionStore}>
{children}
</PositionStoreContext.Provider>
)
}
为了简化访问存储,我们将创建一个 React 自定义钩子,usePositionStore
。此钩子将从上下文中读取存储并允许我们选择状态的特定部分。
¥To simplify accessing the store, we’ll create a React custom hook, usePositionStore
. This hook will read the store from the context and allow us to select specific parts of the state.
ts
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
组件,它将在其容器内渲染一个跟随鼠标光标的点。
¥Now, let's create the MovingDot
component, which will render a dot that follows the mouse cursor within its container.
tsx
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
组件,每个组件都有自己独立的状态。
¥Finally, we'll bring everything together in the App
component, where we render two MovingDot
components, each with its own independent state.
tsx
export default function App() {
return (
<div style={{ display: 'flex' }}>
<PositionStoreProvider>
<MovingDot color="red" />
</PositionStoreProvider>
<PositionStoreProvider>
<MovingDot color="blue" />
</PositionStoreProvider>
</div>
)
}
代码应该是这样的:
¥Here is what the code should look like:
tsx
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 [positionStore] = useState(createPositionStore)
return (
<PositionStoreContext.Provider value={positionStore}>
{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 中使用动态范围(非全局)vanilla 存储
¥Using dynamic scoped (non-global) vanilla stores in React
首先,我们将创建一个工厂函数,生成一个用于管理计数器状态的存储。每个选项卡都有自己的此存储实例。
¥First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store.
ts
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 }))
},
}))
}
接下来,我们将创建一个工厂函数来管理计数器存储的创建和检索。这允许每个选项卡拥有自己独立的计数器。
¥Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter.
ts
const createCounterStoreFactory = (
counterStores: Map<string, ReturnType<typeof createCounterStore>>,
) => {
return (counterStoreKey: string) => {
if (!counterStores.has(counterStoreKey)) {
counterStores.set(counterStoreKey, createCounterStore())
}
return counterStores.get(counterStoreKey)!
}
}
接下来,我们需要一种方法来管理和访问整个应用中的这些存储。我们将为此使用 React 的上下文。
¥Next, we need a way to manage and access these stores throughout our app. We’ll use React’s context for this.
tsx
const CounterStoresContext = createContext(null)
const CounterStoresProvider = ({ children }) => {
const [stores] = useState(
() => new Map<string, ReturnType<typeof createCounterStore>>(),
)
return (
<CounterStoresContext.Provider value={stores}>
{children}
</CounterStoresContext.Provider>
)
}
现在,我们将创建一个自定义钩子 useCounterStore
,它允许我们访问给定选项卡的正确存储。
¥Now, we’ll create a custom hook, useCounterStore
, that lets us access the correct store for a given tab.
tsx
const useCounterStore = <U,>(
currentTabIndex: number,
selector: (state: CounterStore) => U,
) => {
const stores = useContext(CounterStoresContext)
if (stores === undefined) {
throw new Error('useCounterStore must be used within CounterStoresProvider')
}
const getOrCreateCounterStoreByKey = useCallback(
() => createCounterStoreFactory(stores),
[stores],
)
return useStoreWithEqualityFn(
getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`),
selector,
shallow,
)
}
现在,让我们构建 Tabs 组件,用户可以在其中切换选项卡并增加每个选项卡的计数器。
¥Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter.
tsx
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
组件,它将渲染选项卡及其各自的计数器。每个选项卡的计数器状态都是独立管理的。
¥Finally, we'll create the App
component, which renders the tabs and their respective counters. The counter state is managed independently for each tab.
tsx
export default function App() {
return (
<CounterStoresProvider>
<Tabs />
</CounterStoresProvider>
)
}
代码应该是这样的:
¥Here is what the code should look like:
tsx
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>
)
}
故障排除
¥Troubleshooting
TBD