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"),
  }));
}
下一页