00.状态更新
Zustand 基础使用
Zustand 的 Store 就是简单的 Hook,可以将任何东西放在里面:
import { create } from "zustand";
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
你可以在任何地方使用这个 Hook,而不需要提供者。选择你的状态,当该状态改变时,消费组件将重新渲染。
function BearCounter() {
const bears = useStore((state) => state.bears);
return <h1>{bears} around here...</h1>;
}
function Controls() {
const increasePopulation = useStore((state) => state.increasePopulation);
return <button onClick={increasePopulation}>one up</button>;
}
推荐的用法是将行动和状态放在 store 内(让你的行动和你的状态放在一起)。例如:
export const useBoundStore = create((set) => ({
count: 0,
text: 'hello',
inc: () => set((state) => ({ count: state.count + 1 })),
setText: (text) => set({ text }),
})
这创造了一个自成一体的存储,数据和行动在一起。另一种方法是在模块层面上定义动作,在 store 的外部。
export const useBoundStore = create(() => ({
count: 0,
text: "hello",
}));
export const inc = () =>
useBoundStore.setState((state) => ({ count: state.count + 1 }));
export const setText = (text) => useBoundStore.setState({ text });
这有几个好处:
- 它不需要一个 Hook 来调用一个动作;
- 它有利于代码拆分。
虽然这种模式没有任何缺点,但由于它的封装性,有些人可能更喜欢 colocating。不过在 React 18 之前的版本,因为如果 setState 在事件处理程序之外被调用,React 会同步处理它,在事件处理程序之外更新状态会迫使 react 同步更新组件。因此,有可能会遇到 zombie-child 效应。为了解决这个问题,这个动作需要像这样用 unstable_batchedUpdates 来包装:
import { unstable_batchedUpdates } from "react-dom"; // or 'react-native'
const useFishStore = create((set) => ({
fishes: 0,
increaseFishes: () => set((prev) => ({ fishes: prev.fishes + 1 })),
}));
const nonReactCallback = () => {
unstable_batchedUpdates(() => {
useFishStore.getState().increaseFishes();
});
};
状态更新
Flat updates
用 Zustand 更新状态很简单!用新的状态调用所提供的 set 函数,它将与 Store 中现有的状态进行浅层合并。注意 关于嵌套状态见下一节。
type State = {
firstName: string
lastName: string
}
type Action = {
updateFirstName: (firstName: State['firstName']) => void
updateLastName: (lastName: State['lastName']) => void
}
// Create your store, which includes both state and (optionally) actions
const useStore = create<State & Action>((set) => ({
firstName: '',
lastName: '',
updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
updateLastName: (lastName) => set(() => ({ lastName: lastName })),
}))
// In consuming app
function App() {
// "select" the needed state and actions, in this case, the firstName value
// and the action updateFirstName
const [firstName, updateFirstName] = useStore(
(state) => [state.firstName, state.updateFirstName],
shallow
)
return (
<main>
<label>
First name
<input
// Update the "firstName" state
onChange={(e) => updateFirstName(e.currentTarget.value)}
value={firstName}
/>
</label>
<p>
Hello, <strong>{firstName}!</strong>
</p>
</main>
)
}
Deeply nested object
如果你有一个像这样的深层状态对象:
type State = {
deep: {
nested: {
obj: { count: number },
},
},
};
与 React 或 Redux 类似,通常的做法是复制状态对象的每一层。这是用传播操作符…来完成的,并通过手动将其与新的状态值合并在一起。就像这样:
normalInc: () =>
set((state) => ({
deep: {
...state.deep,
nested: {
...state.deep.nested,
obj: {
...state.deep.nested.obj,
count: state.deep.nested.obj.count + 1
}
}
}
})),
许多人使用 Immer 来更新嵌套值。Immer 可以在你需要更新嵌套状态的任何时候使用,比如在 React、Redux,当然还有 Zustand!
你可以使用 Immer 来缩短你对深度嵌套对象的状态更新。让我们来看看一个例子:
immerInc: () =>
set(produce((state: State) => { ++state.deep.nested.obj.count })),
不可变状态与合并
就像 React 的 useState
一样,我们需要永恒地更新状态。下面是一个典型的例子:
import { create } from "zustand";
const useCountStore = create((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}));
set
函数是为了更新 store 中的状态。因为状态是不可改变的,它应该是这样的:
set((state) => ({ ...state, count: state.count + 1 }));
然而,由于这是一个常见的模式,set
实际上是合并状态,我们可以跳过 ...state
部分:
set((state) => ({ count: state.count + 1 }));
set
函数只合并一个层次的状态。如果你有一个嵌套对象,你需要明确地合并它们。你会像这样使用传播操作符模式:
import { create } from "zustand";
const useCountStore = create((set) => ({
nested: { count: 0 },
inc: () =>
set((state) => ({
nested: { ...state.nested, count: state.nested.count + 1 },
})),
}));
要禁用合并行为,你可以为 set
指定一个 replace
的布尔值,像这样:
set((state) => newState, true);
针对 Map 与 Set 进行更新
你需要把 Maps 和 Sets 包在一个对象里面。当你想让它的更新被反映出来时(例如在 React 中),你可以通过对它调用 setState 来实现:
import { create } from "zustand";
const useFooBar = create(() => ({ foo: new Map(), bar: new Set() }));
function doSomething() {
// doing something...
// If you want to update some React component that uses `useFooBar`, you have to call setState
// to let React know that an update happened.
// Following React's best practices, you should create a new Map/Set when updating them:
useFooBar.setState((prev) => ({
foo: new Map(prev.foo).set("newKey", "newValue"),
bar: new Set(prev.bar).add("newKey"),
}));
}