Mutative

常见用法

使用 create() 对对象、数组、Map 和 Set 进行常见的不可变更新。

使用 create() 创建新状态

import { create } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'coding' }],
};

const state = create(baseState, (draft) => {
  draft.list.push({ text: 'learning' });
});

expect(state).not.toBe(baseState);
expect(state.list).not.toBe(baseState.list);
expect(state.list[0]).toBe(baseState.list[0]);

create(baseState, recipe, options?) 为基础状态创建草稿,并将它传给更新函数。更新函数使用普通 JavaScript 修改草稿,create() 返回下一个状态:复制已修改的对象,与基础状态共享其余对象。基础状态保持不变。选项及其他调用方式见 create()。

以下示例像普通 JavaScript 修改状态一样修改草稿。

对象

import { create } from 'mutative';

const baseState = {
  user: { name: 'Ada', address: { city: 'London' } },
  flags: { beta: true } as Record<string, boolean>,
};

const state = create(baseState, (draft) => {
  // Update a nested property.
  draft.user.address.city = 'Paris';
  // Add a property.
  draft.flags.darkMode = true;
  // Delete a property.
  delete draft.flags.beta;
});

expect(state).toEqual({
  user: { name: 'Ada', address: { city: 'Paris' } },
  flags: { darkMode: true },
});

数组

import { create } from 'mutative';

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

const baseState: Todo[] = [
  { id: 1, text: 'learn', done: false },
  { id: 2, text: 'build', done: false },
  { id: 3, text: 'ship', done: false },
];

const state = create(baseState, (draft) => {
  // Update the item with an id.
  const todo = draft.find((item) => item.id === 2);
  if (todo) todo.done = true;

  // Remove the item with an id.
  const index = draft.findIndex((item) => item.id === 1);
  if (index !== -1) draft.splice(index, 1);

  // Insert at the front, and add at the end.
  draft.unshift({ id: 0, text: 'plan', done: true });
  draft.push({ id: 4, text: 'celebrate', done: false });

  // Move the last item to the second position.
  const [moved] = draft.splice(draft.length - 1, 1);
  draft.splice(1, 0, moved);
});

expect(state.map((todo) => todo.id)).toEqual([0, 4, 2, 3]);
expect(state[2].done).toBe(true);
// Unchanged items keep their references.
expect(state[3]).toBe(baseState[2]);

要过滤数组,可将过滤结果赋给草稿。Mutative 会将新数组中的草稿替换为对应的最终值:

import { create } from 'mutative';

const baseState = { todos: [{ text: 'learn', done: true }, { text: 'ship', done: false }] };

const state = create(baseState, (draft) => {
  draft.todos = draft.todos.filter((todo) => !todo.done);
});

expect(state.todos).toEqual([{ text: 'ship', done: false }]);
expect(state.todos[0]).toBe(baseState.todos[1]);

find()、filter() 等带回调的方法通过草稿访问元素,所以每个被访问的元素都会变成草稿。对大数组进行只读搜索时,可改为搜索 current(draft)。

Map 和 Set

import { create } from 'mutative';

const baseState = {
  users: new Map([['ada', { name: 'Ada', age: 36 }]]),
  tags: new Set(['js']),
};

const state = create(baseState, (draft) => {
  // Update a value, and add and delete entries.
  draft.users.get('ada')!.age += 1;
  draft.users.set('grace', { name: 'Grace', age: 45 });
  draft.users.delete('ada');

  draft.tags.add('ts');
  draft.tags.delete('js');
});

expect([...state.users.keys()]).toEqual(['grace']);
expect([...state.tags]).toEqual(['ts']);
// The base state is unchanged.
expect(baseState.users.get('ada')!.age).toBe(36);

从 Map 或 Set 草稿读取的值也是草稿,而 Map 的键保持原样。常见问题中介绍了遍历 Map 草稿、将草稿作为 Map 键和 ES2025 Set 方法。

返回新状态

更新函数可以像 reducer 一样,直接返回下一个状态,而不修改草稿:

import { create, rawReturn } from 'mutative';

const baseState = { count: 5, history: [1, 2, 3] };

const state = create(baseState, (draft) => ({ ...draft, count: 0 }));

expect(state).toEqual({ count: 0, history: [1, 2, 3] });
expect(state.history).toBe(baseState.history);

// `rawReturn()` returns a value as it is, and can return undefined.
const cleared = create(baseState as typeof baseState | undefined, () =>
  rawReturn(undefined)
);

expect(cleared).toBe(undefined);

更新函数必须在修改草稿和返回新值之间二选一,同时进行会抛错。详见返回新状态。

补丁

import { apply, create } from 'mutative';

const baseState = { todos: ['learn'] };

const [state, patches, inversePatches] = create(
  baseState,
  (draft) => {
    draft.todos.push('ship');
  },
  { enablePatches: true }
);

expect(patches).toEqual([{ op: 'add', path: ['todos', 1], value: 'ship' }]);
expect(apply(state, inversePatches)).toEqual(baseState);

补丁格式及撤销、重做的实现见补丁。

在 GitHub 上编辑

最后更新于 作者:unadlib

本页内容