Mutative

在 React 中使用 Mutative

使用 create() 或 use-mutative 的 Hook 更新 React 状态。

当组件状态被新对象替换时,React 会重新渲染,因此更新不能修改当前状态。create() 根据草稿上的修改生成新状态,未变化的部分保留原引用,使经过记忆化处理的子组件和选择器可以跳过这些部分。

配合 useState 和 useReducer

create() 无需适配器:可在 useState 的函数式更新中调用,也可在 useReducer 的 reducer 中调用。

import { useReducer, useState } from 'react';
import { create } from 'mutative';

type Todo = { text: string; done: boolean };

export function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([{ text: 'Learn React', done: false }]);

  const toggle = (index: number) =>
    setTodos((current) =>
      create(current, (draft) => {
        draft[index].done = !draft[index].done;
      })
    );

  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index} onClick={() => toggle(index)}>
          {todo.done ? '✓' : '○'} {todo.text}
        </li>
      ))}
    </ul>
  );
}

type Action = { type: 'add'; text: string } | { type: 'remove'; index: number };

function reducer(state: Todo[], action: Action) {
  return create(state, (draft) => {
    switch (action.type) {
      case 'add':
        draft.push({ text: action.text, done: false });
        break;
      case 'remove':
        draft.splice(action.index, 1);
        break;
    }
  });
}

export function useTodos(initialTodos: Todo[]) {
  return useReducer(reducer, initialTodos);
}

如果更新函数没有修改任何内容,就返回当前状态本身,React 会跳过重新渲染。

use-mutative

use-mutative 将上述模式封装为两个 Hook。

npm install mutative use-mutative

useMutative()

useMutative() 的用法类似 useState(),其 setter 还可以接收修改草稿的更新函数:

import { useMutative } from 'use-mutative';

export function App() {
  const [state, setState] = useMutative({
    foo: 'bar',
    list: [{ text: 'todo' }],
  });

  return (
    <>
      <button
        onClick={() => {
          // Change a draft of the state.
          setState((draft) => {
            draft.foo = `${draft.foo} 2`;
            draft.list.push({ text: 'todo 2' });
          });
        }}
      >
        Update
      </button>
      <button
        onClick={() => {
          // Or replace the state.
          setState({ foo: 'bar 2', list: [{ text: 'todo 2' }] });
        }}
      >
        Replace
      </button>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </>
  );
}

useMutativeReducer()

useMutativeReducer() 的用法类似 useReducer(),但 reducer 接收到的是状态草稿。reducer 可以修改草稿并不返回任何值,或直接返回新状态。如果返回值不包含草稿,可用 rawReturn() 包裹,跳过对其中草稿的搜索。

import { rawReturn, type Draft } from 'mutative';
import { useMutativeReducer } from 'use-mutative';

const initialState = { count: 0 };

type Action = { type: 'reset' | 'increment' | 'decrement' };

function reducer(draft: Draft<typeof initialState>, action: Action) {
  switch (action.type) {
    case 'reset':
      return rawReturn(initialState);
    case 'increment':
      draft.count += 1;
      return;
    case 'decrement':
      draft.count -= 1;
      return;
  }
}

export function Counter() {
  const [state, dispatch] = useMutativeReducer(reducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

选项与补丁

两个 Hook 都将 create() 的选项作为最后一个参数。开启 enablePatches 后,还会返回最近一次更新的正向补丁和逆向补丁:

const [state, setState, patches, inversePatches] = useMutative(initialState, {
  enablePatches: true,
});

const [state, dispatch, patches, inversePatches] = useMutativeReducer(
  reducer,
  initialState,
  undefined,
  { enablePatches: true }
);

useMutativeReducer() 的第三个参数是 useReducer() 的可选初始化函数。

撤销与重做

use-travel 通过每次更新的补丁维护状态历史,支持撤销、重做和重置:

import { useTravel } from 'use-travel';

export function Counter() {
  const [state, setState, controls] = useTravel({ count: 0 });

  return (
    <div>
      <strong>{state.count}</strong>
      <button
        onClick={() =>
          setState((draft) => {
            draft.count += 1;
          })
        }
      >
        Increment
      </button>
      <button onClick={() => controls.back()} disabled={!controls.canUndo}>
        Undo
      </button>
      <button onClick={() => controls.forward()} disabled={!controls.canRedo}>
        Redo
      </button>
    </div>
  );
}

Zustand、Jotai、XState 等状态库也有 Mutative 集成,详见生态系统。

在 GitHub 上编辑

最后更新于 作者:unadlib

本页内容