From 607b9943644dd30a385cadf9f76db70e65ca3a10 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 31 Jan 2022 18:29:05 +0000 Subject: [PATCH 1/9] chore: added unit test for #897 --- __tests__/patch.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/__tests__/patch.js b/__tests__/patch.js index bbe5fed8..9c112f8e 100644 --- a/__tests__/patch.js +++ b/__tests__/patch.js @@ -1417,3 +1417,13 @@ describe("#879 delete item from array - 2", () => { [1, 2, undefined] ) }) + +test("#897 appendPatch", () => { + const state0 = {a: []} + const state1 = applyPatches(state0, [{op: "add", path: ["a", "-"], value: 1}]) + const state2 = applyPatches(state1, [{op: "add", path: ["a", "-"], value: 2}]) + const state3 = applyPatches(state2, [{op: "add", path: ["a", "-"], value: 3}]) + expect(state3).toEqual({ + a: [1, 2, 3] + }) +}) From 53c2f9129781e6535c343adc7bf3681656df4920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=84=BF=E6=97=B6?= <58261676+childrentime@users.noreply.github.com> Date: Fri, 11 Mar 2022 22:34:25 +0800 Subject: [PATCH 2/9] docs: add chinese translation (#902) * config: add i18n * docs: add chinese translation * theming improvement for dropdown --- website/docusaurus.config.js | 10 +- .../current.json | 18 ++ .../current/api.md | 52 ++++ .../current/async.mdx | 71 ++++++ .../current/built-with.md | 24 ++ .../current/complex-objects.md | 74 ++++++ .../current/current.md | 62 +++++ .../current/curried-produce.mdx | 79 ++++++ .../current/example-setstate.mdx | 241 ++++++++++++++++++ .../current/faq.md | 46 ++++ .../current/freezing.mdx | 39 +++ .../current/installation.mdx | 82 ++++++ .../current/introduction.md | 103 ++++++++ .../current/map-set.md | 60 +++++ .../current/original.md | 36 +++ .../current/patches.mdx | 230 +++++++++++++++++ .../current/performance.mdx | 83 ++++++ .../current/pitfalls.md | 90 +++++++ .../current/produce.mdx | 91 +++++++ .../current/resources.md | 18 ++ .../current/return.mdx | 152 +++++++++++ .../current/support.md | 13 + .../current/typescript.mdx | 210 +++++++++++++++ .../current/update-patterns.md | 142 +++++++++++ .../docusaurus-theme-classic/footer.json | 6 + .../docusaurus-theme-classic/navbar.json | 18 ++ website/src/css/immer-infima.css | 4 + 27 files changed, 2053 insertions(+), 1 deletion(-) create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current.json create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/api.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/async.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/built-with.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/complex-objects.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/current.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/curried-produce.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/example-setstate.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/faq.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/freezing.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/installation.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/introduction.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/map-set.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/original.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/patches.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/performance.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/pitfalls.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/produce.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/resources.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/return.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/support.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/typescript.mdx create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/update-patterns.md create mode 100644 website/i18n/zh-CN/docusaurus-theme-classic/footer.json create mode 100644 website/i18n/zh-CN/docusaurus-theme-classic/navbar.json diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 26e87a4c..96d19260 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -10,6 +10,10 @@ module.exports = { onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", favicon: "img/favicon.ico", + i18n: { + defaultLocale: 'en', + locales: ['en', 'zh-CN'], + }, themeConfig: { googleAnalytics: { trackingID: "UA-65632006-3", @@ -39,7 +43,11 @@ module.exports = { docId: "support", label: "Support Immer", position: "right" - } + }, + { + type: 'localeDropdown', + position: 'left', + }, ] }, footer: { diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current.json b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current.json new file mode 100644 index 00000000..3633ca0e --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,18 @@ +{ + "version.label": { + "message": "Next", + "description": "The label for version current" + }, + "sidebar.Immer.category.Basics": { + "message": "基础", + "description": "The label for category Basics in sidebar Immer" + }, + "sidebar.Immer.category.Advanced Features": { + "message": "高级", + "description": "The label for category Advanced Features in sidebar Immer" + }, + "sidebar.Immer.category.Resources": { + "message": "资源", + "description": "The label for category Resources in sidebar Immer" + } +} \ No newline at end of file diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/api.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/api.md new file mode 100644 index 00000000..2bd17300 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/api.md @@ -0,0 +1,52 @@ +--- +id: api +title: API 概览 +--- + +
+
+
+ +| 导出名称 | 描述 | 章节 | +| --- | --- | --- | +| `(default)` | Immer 核心 API,通常命名为 `produce`: `import produce from "immer"` | [Produce](./produce.mdx) | +| `applyPatches` | 给定一个基本 state 或 draft,以及一组 patches ,应用 patches | [Patches](./patches.mdx) | +| `castDraft` | 将任何不可变类型转换为其可变对应物。这只是一个转换,实际上并没有做任何事情。 | [TypeScript](./typescript.mdx) | +| `castImmutable` | 将任何可变类型转换为其不可变对应物。这只是一个转换,实际上并没有做任何事情。 | [TypeScript](./typescript.mdx) | +| `createDraft` | 给定一个基本 state,创建一个可变 draft,任何修改都将被记录下来 | [Async](./async.mdx) | +| `current` | 给定一个 draft 对象(不必是对象的根结点),对 draft 的当前状态进行快照 | [Current](./current.md) | +| `Draft` | 暴露的 TypeScript 类型以将不可变类型转换为可变类型 | [TypeScript](./typescript.mdx) | +| `enableAllPlugins()` | 启用下面提到的所有插件 | [Installation](./installation.mdx#pick-your-immer-version) | +| `enableES5()` | 启用对旧 JavaScript 引擎的支持,例如 Internet Explorer 和 React Native | [Installation](./installation.mdx#pick-your-immer-version) | +| `enableMapSet()` | 启用对 `Map` 和 `Set` 集合的支持。 | [Installation](./installation.mdx#pick-your-immer-version) | +| `enablePatches()` | 启用对 JSON patches 的支持 | [Installation](./installation#pick-your-immer-version) | +| `finishDraft` | 给定使用 `createDraft` 创建的 draft,冻结 draft 并生成并返回下一个不可变状态,该状态捕获所有更改 | [Async](./async.mdx) | +| `freeze(obj, deep?)` | 冻结可 draft 对象。返回原始对象。默认情况下浅冻结,但如果第二个参数为真,它将递归冻结。| +| `Immer` | 可用于创建第二个“immer”实例(暴露此实例中列出的所有 API)的构造函数,它不与全局实例共享其设置 | +| `immerable` | 可以添加到构造函数或原型的符号,表示 Immer 应该将类视为可以安全 draft的东西 | [Classes](./complex-objects.md) | +| `Immutable` | 暴露的 TypeScript 类型以将可变类型转换为不可变类型 | | +| `isDraft` | 如果给定对象是 draft 对象,则返回 true | | +| `isDraftable` | 如果 Immer 能够将此对象变成 draft,则返回 true。这适用于:数组、没有原型的对象、以 `Object` 为原型的对象、在其构造函数或原型上具有 `immerable` 符号的对象 | | +| `nothing` | 可以从 recipe 返回的值,以指示应生成 `undefined` | [Return](./return.mdx) | +| `original` | 给定一个 draft 对象(不必是对象的根结点),返回原始状态树中相同路径的原始对象(如果存在) | [Original](./original.md) | +| `Patch` | 暴露的 TypeScript 类型,描述(反向)patches 对象的形状 | [Patches](./patches.mdx) | +| `produce` | Immer 的核心 API,也暴露为 `default` 导出 | [Produce](./produce.mdx) | +| `produceWithPatches` | 与 `produce` 相同,但它不仅返回生成的对象,还返回一个由 `[result, patch, inversePatches]` 组成的元组 | [Patches](./patches.mdx) | +| `setAutoFreeze` | 启用/禁用递归的自动冻结。默认启用 | [Freezing](./freezing.mdx) | +| `setUseProxies` | 可用于禁用或强制使用 `Proxy` 对象。在提交错误报告时很有用。 | | + +## 导入 immer + +`produce` 作为默认导出,但也可以选择将其用作名称导入,因为这有利于一些较旧的项目设置。所以下面的导入都是正确的,这里推荐第一个: + +```javascript +import produce from "immer" +import {produce} from "immer" + +const {produce} = require("immer") +const produce = require("immer").produce +const produce = require("immer").default + +import unleashTheMagic from "immer" +import {produce as unleashTheMagic} from "immer" +``` diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/async.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/async.mdx new file mode 100644 index 00000000..e1af89a8 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/async.mdx @@ -0,0 +1,71 @@ +--- +id: async +title: 异步 producers & createDraft / finishDraft +sidebar_label: 异步 produce / createDraft +--- + +
+
+
+ + egghead.io 第11课: 创建异步 producers(以及为什么不应该这样做) + +
+
+ +
+ + Hosted on egghead.io + +
+ +允许从 recipe 返回 Promise 对象。或者,换句话说,使用 `async / await`。这对于长时间运行的进程非常有用,只有在 Promise 链解析后才生成新对象。请注意,如果 producer 是异步的,`produce` 本身(即使是柯里化形式)也会返回一个 promise。例子: + +```javascript +import produce from "immer" + +const user = { + name: "michel", + todos: [] +} + +const loadedUser = await produce(user, async function(draft) { + draft.todos = await (await window.fetch("http://host/" + draft.name)).json() +}) +``` + +_警告:请注意,draft 不应从异步程序中“泄露”并存储在其他地方。异步过程完成后,draft 仍将被释放_ + +## `createDraft` and `finishDraft` + +`createDraft` 和 `finishDraft` 是两个底层函数,它们对于在 immer 之上构建抽象的库非常有用。它避免了为了使用 draft 始终创建函数。相反,人们可以创建一个 draft,对其进行修改,并在未来的某个时间完成该 draft,在这种情况下,将产生下一个不可变状态。例如,我们可以将上面的示例重写为: + + +```javascript +import {createDraft, finishDraft} from "immer" + +const user = { + name: "michel", + todos: [] +} + +const draft = createDraft(user) +draft.todos = await (await window.fetch("http://host/" + draft.name)).json() +const loadedUser = finishDraft(draft) +``` + +注意:`finishDraft` 以一个 `patchListener` 作为第二个参数,可以用来记录 patches,类似于 `produce` + +_警告:一般情况下,我们建议使用 `producer` 而不是 `createDraft / finishDraft` 组合,`produce` 在使用中不易出错,并且在代码中更清楚地区分了可变性和不变性的概念。_ diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/built-with.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/built-with.md new file mode 100644 index 00000000..a5ed3248 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/built-with.md @@ -0,0 +1,24 @@ +--- +id: built-with +title: 基于 Immer +--- + +
+
+
+ +- [react-copy-write](https://github.com/aweary/react-copy-write) _具有可变 API 的不可变状态_ +- [redux-toolkit](https://github.com/reduxjs/redux-toolkit) _官方的,opinionated,自带全套工具的效率 Redux 开发_ +- [immer based handleActions](https://gist.github.com/kitze/fb65f527803a93fb2803ce79a792fff8) _Redux 自由动作脚手架_ +- [redux-box](https://github.com/anish000kumar/redux-box) _模块化且易于掌握的基于 redux 的状态管理,样板代码最少_ +- [quick-redux](https://github.com/jeffreyyoung/quick-redux) _使 redux 开发更快更容易的工具_ +- [bey](https://github.com/jamiebuilds/bey) _使用 Immer 实现 React 的简单不可变状态_ +- [cool-store](https://github.com/Maxvien/cool-store) _CoolStore 是建立在 ImmerJS 和 RxJS 之上的不可变状态存储_ +- [immer-wieder](https://github.com/drcmda/immer-wieder#readme) _结合 React 16 Context 和 immer 用于 Redux 语义的状态管理库_ +- [robodux](https://github.com/neurosnap/robodux) _减少 redux 样板的灵活脚手架_ +- [immer-reducer](https://github.com/epeli/immer-reducer) _用于 React Hooks 和 Redux 的 Typescript 类型安全和简洁的 reducer_ +- [redux-ts-utils](https://github.com/knpwrs/redux-ts-utils) _使用 Redux 创建类型安全的应用程序所需的一切,强调简单性_ +- [react-state-tree](https://github.com/suchipi/react-state-tree) _将您的状态持久化到类似 redux 的状态树,useState 的替代品_ +- [redux-immer](https://github.com/salvoravida/redux-immer) _用于创建与 immer 状态一起使用的 Redux combineReducers 的等效函数。像 redux-immutable 但是 immer的_ +- [ngrx-wieder](https://github.com/nilsmehlhorn/ngrx-wieder) _轻量级但可配置的解决方案,用于在 NgRx 和 Immer 之上的 Angular 应用程序中实现撤消重做_ +- ... 还有 [很多](https://www.npmjs.com/browse/depended/immer) diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/complex-objects.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/complex-objects.md new file mode 100644 index 00000000..089e2bc7 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/complex-objects.md @@ -0,0 +1,74 @@ +--- +id: complex-objects +title: 类 +--- + +
+
+
+ +普通对象(没有原型的对象)、数组、`Map` 和 `Set` 总是可以用 Immer 更新。所有其他对象都必须使用 `immerable` 符号将自己标记为与 Immer 兼容。当这些对象之一在 `produce` 中进行更改时,它的原型将保留在副本之间 + + +```js +import {immerable} from "immer" + +class Foo { + [immerable] = true // 方式一 + + constructor() { + this[immerable] = true // 方式二 + } +} + +Foo[immerable] = true // 方式三 +``` + +### 例子 + +```js +import {immerable, produce} from "immer" + +class Clock { + [immerable] = true + + constructor(hour, minute) { + this.hour = hour + this.minute = minute + } + + get time() { + return `${this.hour}:${this.minute}` + } + + tick() { + return produce(this, draft => { + draft.minute++ + }) + } +} + +const clock1 = new Clock(12, 10) +const clock2 = clock1.tick() +console.log(clock1.time) // 12:10 +console.log(clock2.time) // 12:11 +console.log(clock2 instanceof Clock) // true +``` + +### 语义细节 + +关于类的 `draft` 对象语义如下: + +1. 类的 `draft` 是一个新对象,但与原始对象具有相同的原型。 +2. 创建 `draft` 时,Immer 会将所有拥有的的属性从源对象复制到 `draft`。这包括不可枚举和符号属性。 +3. 源对象拥有的 getter 将在复制过程中被调用,就像 `Object.assign` 方法一样 +4. 继承的 getter 和方法将保持原样并被 `draft` 继承 +5. Immer 不会调用构造函数 +6. 最终实例将使用与创建 `draft` 相同的机制构建。 +7. 只有具有 setter 的 getter 才能在 `draft` 中写入,否则无法将值复制回来。 + +因为 Immer 会将对象拥有的 getter 解引用到普通属性中,所以可以使用在其字段上使用 getter/setter 获得的对象,就像MobX 和 Vue。 + +Immer 不支持外来/引擎原生对象,例如 DOM 节点或 Buffers,也不支持继承的 Map、Set 或数组,并且不能在它们上使用 immerable 符号。 + +因此,例如在使用 `Date` 对象时,您应该始终创建一个新的 `Date` 实例,而不是改变现有的 `Date` 对象。 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/current.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/current.md new file mode 100644 index 00000000..170a16d9 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/current.md @@ -0,0 +1,62 @@ +--- +id: current +title: 从 draft 中提取当前 state +sidebar_label: Current +--- + +
+
+
+ +Immer 暴露了一个命名导出的 `current`函数,可以创建 draft 对象当前状态的一个副本。 +这对于调试非常有用(因为这些对象不会是代理对象,也不会被记录下来)。 +此外,对 `current` 的引用可以安全地从 `produce` 函数中释放。换句话说,`current` 提供 draft 当前状态的快照。 + +`current` 工作生成的对象类似于 `produced` 本身创建的对象。 + + +1. 未修改的对象将在结构上与原始对象共享。 +2. 如果未对 draft 进行任何更改,通常它会保留 original(draft) === current(draft),但这并不能保证。 +3. 未来对 draft 的更改不会反映在 `current` 生成的对象中(不可被 draft 对象的引用除外) +4. 与 `produce` 创建的对象不同,`current` 创建的对象不会被冻结。 + +谨慎使用 `current`,这可能是一项潜在的昂贵操作,尤其是在使用 ES5 时。 + +请注意,不能在不是 draft 的对象上调用 `current`。 + +### 例子 + +以下示例显示了 `current`(和 `original` )的效果: + + +```js +const base = { + x: 0 +} + +const next = produce(base, draft => { + draft.x++ + const orig = original(draft) + const copy = current(draft) + console.log(orig.x) + console.log(copy.x) + + setTimeout(() => { + // 将在 produce 完成后执行 + console.log(orig.x) + console.log(copy.x) + }, 100) + + draft.x++ + console.log(draft.x) +}) +console.log(next.x) + +// 将会打印 +// 0 (orig.x) +// 1 (copy.x) +// 2 (draft.x) +// 2 (next.x) +// 0 (after timeout, orig.x) +// 1 (after timeout, copy.x) +``` diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/curried-produce.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/curried-produce.mdx new file mode 100644 index 00000000..88af75e3 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/curried-produce.mdx @@ -0,0 +1,79 @@ +--- +id: curried-produce +title: 柯里化 producers +--- + +
+
+
+ + egghead.io 第六课: 使用柯里化简化代码 + +
+
+ +
+ + Hosted on egghead.io + +
+ +将函数作为第一个参数传递给 `produce` 会创建一个函数,该函数尚未将 `produce` 应用于特定 state,而是创建一个函数,该函数将应用于将来传递给它的任何 state。这通常称为柯里化。举个例子: + +```javascript +import produce from "immer" + +function toggleTodo(state, id) { + return produce(state, draft => { + const todo = draft.find(todo => todo.id === id) + todo.done = !todo.done + }) +} + +const baseState = [ + { + id: "JavaScript", + title: "Learn TypeScript", + done: true + }, + { + id: "Immer", + title: "Try Immer", + done: false + } +] + +const nextState = toggleTodo(baseState, "Immer") +``` + +上面的 `toggleTodo` 模式非常典型;传递一个现有的 state 来 `produce`,修改 `draft`,然后返回结果。由于 `state` 除了将其传递给 `produce` 之外没有其他任何用途,因此可以通过使用 `produce` 的柯里化形式来简化上面的示例,其中您只传递 `produce` recipe 函数,并且 `produce` 将返回一个应用 recipe 到基础状态的新函数。这允许我们缩短上述 `toggleTodo` 定义。 + +```javascript +import produce from "immer" + +// curried producer: +const toggleTodo = produce((draft, id) => { + const todo = draft.find(todo => todo.id === id) + todo.done = !todo.done +}) + +const baseState = [ + /* as is */ +] + +const nextState = toggleTodo(baseState, "Immer") +``` + +请注意,`id` 参数现在已成为 recipe 函数的一部分!这种拥有 curried producers 的模式与 React 中的 `useState` Hook 非常巧妙地结合在一起,我们将在下一页看到。 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/example-setstate.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/example-setstate.mdx new file mode 100644 index 00000000..3d0d77c8 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/example-setstate.mdx @@ -0,0 +1,241 @@ +--- +id: example-setstate +title: React & Immer +--- + +
+
+
+
+ + egghead.io 第八课: 使用 Immer 和 useState,或者 useImmer。 + +
+
+ +
+ + Hosted on egghead.io + +
+ +## useState + Immer + +`useState` hook 假定存储在其中的任何 state 都被视为不可变的。使用 Immer 可以大大简化 React 组件状态的深度更新。下面的例子展示了如何使用 `produce` 和 `useState` ,可以在 [CodeSandbox](https://codesandbox.io/s/immer-usestate-ujkgg?file=/src/index.js) 试试。 + +```javascript +import React, { useCallback, useState } from "react"; +import produce from "immer"; + +const TodoList = () => { + const [todos, setTodos] = useState([ + { + id: "React", + title: "Learn React", + done: true + }, + { + id: "Immer", + title: "Try Immer", + done: false + } + ]); + + const handleToggle = useCallback((id) => { + setTodos( + produce((draft) => { + const todo = draft.find((todo) => todo.id === id); + todo.done = !todo.done; + }) + ); + }, []); + + const handleAdd = useCallback(() => { + setTodos( + produce((draft) => { + draft.push({ + id: "todo_" + Math.random(), + title: "A new todo", + done: false + }); + }) + ); + }, []); + + return (
{*/ See CodeSandbox */}
) +} +``` + +## useImmer + +由于所有 state 的更新都使用 `produce` 包装的更新模式,所以我们可以通过将更新模式包装在 [use-immer](https://www.npmjs.com/package/use-immer) 包中来简化上述操作 + +```javascript +import React, { useCallback } from "react"; +import { useImmer } from "use-immer"; + +const TodoList = () => { + const [todos, setTodos] = useImmer([ + { + id: "React", + title: "Learn React", + done: true + }, + { + id: "Immer", + title: "Try Immer", + done: false + } + ]); + + const handleToggle = useCallback((id) => { + setTodos((draft) => { + const todo = draft.find((todo) => todo.id === id); + todo.done = !todo.done; + }); + }, []); + + const handleAdd = useCallback(() => { + setTodos((draft) => { + draft.push({ + id: "todo_" + Math.random(), + title: "A new todo", + done: false + }); + }); + }, []); + + // etc +``` + +完整的 demo 请参阅 [CodeSandbox](https://codesandbox.io/s/use-immer-bvd5v?file=/src/index.js) + +## useReducer + Immer + +与 `useState` 类似,`useReducer` 也与 Immer 巧妙结合,如 [CodeSandbox](https://codesandbox.io/s/immer-usereducer-bqpzn?file=/src/index.js:0-1018) 所示: + +```javascript +import React, {useCallback, useReducer} from "react" +import produce from "immer" + +const TodoList = () => { + const [todos, dispatch] = useReducer( + produce((draft, action) => { + switch (action.type) { + case "toggle": + const todo = draft.find(todo => todo.id === action.id) + todo.done = !todo.done + break + case "add": + draft.push({ + id: action.id, + title: "A new todo", + done: false + }) + break + default: + break + } + }), + [ + /* initial todos */ + ] + ) + + const handleToggle = useCallback(id => { + dispatch({ + type: "toggle", + id + }) + }, []) + + const handleAdd = useCallback(() => { + dispatch({ + type: "add", + id: "todo_" + Math.random() + }) + }, []) + + // etc +} +``` + +## useImmerReducer + +同上,可以通过 `use-immer` 包中的 `useImmerReducer` 简化 ([demo](https://codesandbox.io/s/useimmerreducer-sycpb?file=/src/index.js)) + +```javascript +import React, { useCallback } from "react"; +import { useImmerReducer } from "use-immer"; + +const TodoList = () => { + const [todos, dispatch] = useImmerReducer( + (draft, action) => { + switch (action.type) { + case "toggle": + const todo = draft.find((todo) => todo.id === action.id); + todo.done = !todo.done; + break; + case "add": + draft.push({ + id: action.id, + title: "A new todo", + done: false + }); + break; + default: + break; + } + }, + [ /* initial todos */ ] + ); + + //etc + +``` + + +## Redux + Immer + +Redux + Immer 在 [Redux Toolkit](https://redux-toolkit.js.org/usage/immer-reducers) 的文档中被广泛介绍。对于没有 Redux Toolkit 的 Redux,可以应用与上面应用于 `useReducer` 相同的技巧:使用 `produce` 包装 reducer 函数,您可以安全地修改 draft! + +例子: + +```javascript +import produce from "immer" + +// 初始 state +const INITIAL_STATE = [ + /* 一系列 todos */ +] + +const todosReducer = produce((draft, action) => { + switch (action.type) { + case "toggle": + const todo = draft.find(todo => todo.id === action.id) + todo.done = !todo.done + break + case "add": + draft.push({ + id: action.id, + title: "A new todo", + done: false + }) + break + default: + break + } +}) +``` diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/faq.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/faq.md new file mode 100644 index 00000000..c718fb7c --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/faq.md @@ -0,0 +1,46 @@ +--- +id: faq +title: 常见问题 +sidebar_label: FAQ +--- + +
+
+
+ +## Q: Immer 如何工作 + +阅读[介绍博客](https://medium.com/@mweststrate/introducing-immer-immutability-the-easy-way-9d73d8f71cb3)的(第二部分) + + +## Q: Immer 是否使用结构共享?这样我的选择器就可以被记住了吗? + +A: 是的 + +## Q: Immer 是否支持深度更新? + +A: 是的 + +## Q: 我的目标环境中没有代理。我可以使用 Immer 吗? + +A: 可以 - [查看细节](./installation.mdx#immer-on-older-javascript-environments) + +## Q: 使用 Immer 时可以对我的数据结构进行类型检查吗? + +A: 可以 + +## Q: 使用 Immer 时,我可以在状态树中存储 `Date` 对象、函数等吗? + +A: 可以 + +## Q: 我可以使用 Map 和 Sets 吗? + +A: 可以 + +## Q: 快吗? + +A: 快 + +## Q: 灵感! Immer 可以为我冻结状态吗? + +A: 可以 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/freezing.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/freezing.mdx new file mode 100644 index 00000000..cc805cd6 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/freezing.mdx @@ -0,0 +1,39 @@ +--- +id: freezing +title: 自动冻结 +--- + +
+
+
+ + egghead.io 第7课: Immer 自动冻结数据 + +
+
+ +
+ + Hosted on egghead.io + +
+ +Immer 自动冻结所有使用 `produce` 修改的任何 state。这可以防止在 producer 之外意外修改 state。在大多数情况下,这是最佳实践,但你可以通过 `setAutoFreeze(true / false)` 显式打开或关闭此功能。 + +Immer 永远不会冻结不可枚举、非自己或符号属性的(内容),除非它们的内容是可以被 `draft` 的。 + +_⚠️ Immer 以递归方式冻结所有内容,对于将来不会更改的大型数据对象,这可能会矫枉过正,在这种情况下,使用 `freeze` 函数 浅层冻结数据会更有效。⚠️_ + +_⚠️ 如果启用了自动冻结,recipe 函数并非完全没有副作用:任何最终出现在 produce 结果中的普通对象或数组都将被冻结,即使这些对象在 producer 开始之前没有被冻结!⚠️_ diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/installation.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/installation.mdx new file mode 100644 index 00000000..3de67cf2 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/installation.mdx @@ -0,0 +1,82 @@ +--- +id: installation +title: 安装 +--- + +
+
+
+ +Immer 可以作为直接依赖项安装,并且可以在任何 ES5 环境中工作: + +- Yarn: `yarn add immer` +- NPM: `npm install immer` +- CDN: 暴露的全局变量是 `immer` + - Unpkg: `` + - JSDelivr: `` + - ⚠️ 使用 CDN 时,最好检查浏览器中的 url 并查看它解析为哪个版本,这样当更新发布时,您的用户不会意外地获得更新的版本。因此,请改用如下网址:https://unpkg.com/immer@6.0.3/dist/immer.umd.production.min.js。在 URL 中将 production.min 替换为 development 以进行开发构建。 + +## 选择您的 Immer 版本 + +_本节仅适用于版本 6 及更高版本_ + +为确保 Immer 尽可能小,并非每个项目都需要的功能已选择加入,并且必须明确启用。这可确保在将您的应用程序捆绑用于生产时,未使用的功能不会占用任何空间。 + +可以选择加入以下功能: + +| 功能 | 描述 | 调用方法 | +| --- | --- | --- | +| ES 5 支持 | 如果您的应用程序需要能够在较旧的 JavaScript 环境(例如 Internet Explorer 或 React Native)上运行,请启用此功能。 | `enableES5()` | +| [ES2015 Map and Set 支持](./complex-objects.md) | 要使 Immer 能够对原生 Map 和 Set 集合进行操作,请启用此功能 | `enableMapSet()` | +| [JSON 补丁 支持](./patches.mdx) | Immer 可以跟踪您对 draft 对象所做的所有更改。这对于使用 JSON 补丁时传达更改很有用 | `enablePatches()` | +| **All of the above** | 不确定您需要哪些功能?我们建议在新项目上默认启用上述所有功能。几个 KB 的过早优化可能不值得一开始的麻烦。此外,启用或禁用功能不会影响 Immer 本身的性能 | `enableAllPlugins()` | + +例如,如果您想在 `Map` 上使用 `produce` ,则需要在应用程序启动期间启用此功能一次: + +```typescript +// 在你的应用程序入口文件 +import {enableMapSet} from "immer" + +enableMapSet() + +// ...然后 +import produce from "immer" + +const usersById_v1 = new Map([ + ["michel", {name: "Michel Weststrate", country: "NL"}] +]) + +const usersById_v2 = produce(usersById_v1, draft => { + draft.get("michel").country = "UK" +}) + +expect(usersById_v1.get("michel").country).toBe("NL") +expect(usersById_v2.get("michel").country).toBe("UK") +``` + +Immer 以大约 3KB 的 gzip 压缩开始。每个启用的插件都会增加 < 1 KB。细分如下 + +``` +Import size report for immer: +┌───────────────────────┬───────────┬────────────┬───────────┐ +│ (index) │ just this │ cumulative │ increment │ +├───────────────────────┼───────────┼────────────┼───────────┤ +│ import * from 'immer' │ 5662 │ 0 │ 0 │ +│ produce │ 3100 │ 3100 │ 0 │ +│ enableES5 │ 3761 │ 3770 │ 670 │ +│ enableMapSet │ 3885 │ 4527 │ 757 │ +│ enablePatches │ 3891 │ 5301 │ 774 │ +│ enableAllPlugins │ 5297 │ 5348 │ 47 │ +└───────────────────────┴───────────┴────────────┴───────────┘ +(this report was generated by npmjs.com/package/import-size) +``` + +## Immer 使用在旧的 JavaScript 环境? + +默认情况下,`produce` 尝试使用代理以获得最佳性能。但是,在较旧的 JavaScript 引擎上,代理不可用。例如,在 Android 上运行 Microsoft Internet Explorer 或 React Native(如果 React Native < v0.59 或在 React Native < v0.64 上使用 Hermes 引擎)时。在这种情况下,Immer 将回退到与 ES5 兼容的实现,其工作方式相同,但速度稍慢 + +从版本 6 开始,必须通过调用 `enableES5()` 显式启用对回退实现的支持 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/introduction.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/introduction.md new file mode 100644 index 00000000..84b587a8 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/introduction.md @@ -0,0 +1,103 @@ +--- +id: introduction +title: Immer 入门 +sidebar_label: 入门 +slug: / +--- + +
+
+
+ + + +# Immer + +2019 年 “年度突破”[React 开源奖](https://osawards.com/react/)和“最有影响的贡献”[JavaScript 开源奖](https://osawards.com/javascript/)的获得者 + +- 介绍博客: [Immer: Immutability the easy way](https://medium.com/@mweststrate/introducing-immer-immutability-the-easy-way-9d73d8f71cb3) +- Egghead.io 简短课程,涵盖 Immer 的基本知识: [Simplify creating immutable data trees with Immer (7 分钟)](https://egghead.io/lessons/redux-simplify-creating-immutable-data-trees-with-immer) +- Egghead.io 免费深入课程: [Immutable JavaScript Data Structures with Immer (58 分钟)](https://egghead.io/courses/immutable-javascript-data-structures-with-immer) + +--- + +Immer(德语为:always)是一个小型包,可让您以更方便的方式使用不可变状态。 + +### Immer 简化了不可变数据结构的处理 + +Immer 可以在需要使用不可变数据结构的任何上下文中使用。例如与 React state、React 或 Redux reducers 或者 configuration management 结合使用。不可变的数据结构允许(高效)的变化检测:如果对对象的引用没有改变,那么对象本身也没有改变。此外,它使克隆对象相对便宜:数据树的未更改部分不需要复制,并且在内存中与相同状态的旧版本共享 + +一般来说,这些好处可以通过确保您永远不会更改对象、数组或映射的任何属性来实现,而是始终创建一个更改后的副本。在实践中,这可能会导致代码编写起来非常麻烦,并且很容易意外违反这些约束。 Immer 将通过解决以下痛点来帮助您遵循不可变数据范式: + +1. Immer 将检测到意外 mutations 并抛出错误。 +2. Immer 将不再需要创建对不可变对象进行深度更新时所需的典型样板代码:如果没有 Immer,则需要在每个级别手动制作对象副本。通常通过使用大量 `...` 展开操作。使用 Immer 时,会对 `draft` 对象进行更改,该对象会记录更改并负责创建必要的副本,而不会影响原始对象。 +3. 使用 Immer 时,您无需学习专用 API 或数据结构即可从范例中受益。使用 Immer,您将使用纯 JavaScript 数据结构,并使用众所周知的安全地可变 JavaScript API。 + +### 一个简单的比较示例 + +```javascript +const baseState = [ + { + title: "Learn TypeScript", + done: true + }, + { + title: "Try Immer", + done: false + } +] +``` + +假设我们有上述基本状态,我们需要更新第二个 todo,并添加第三个。但是,我们不想改变原始的 baseState,我们也想避免深度克隆(以保留第一个 todo) + +#### 不使用 Immer + +如果没有 Immer,我们将不得不小心地浅拷贝每层受我们更改影响的 state 结构 + +```javascript +const nextState = baseState.slice() // 浅拷贝数组 +nextState[1] = { + // 替换第一层元素 + ...nextState[1], // 浅拷贝第一层元素 + done: true // 期望的更新 +} +// 因为 nextState 是新拷贝的, 所以使用 push 方法是安全的, +// 但是在未来的任意时间做相同的事情会违反不变性原则并且导致 bug! +nextState.push({title: "Tweet about it"}) +``` + +#### 使用 Immer + +使用 Immer,这个过程更加简单。我们可以利用 `produce` 函数,它将我们要更改的 state 作为第一个参数,对于第二个参数,我们传递一个名为 recipe 的函数,该函数传递一个 `draft` 参数,我们可以对其应用直接的 `mutations`。一旦 `recipe` 执行完成,这些 `mutations` 被记录并用于产生下一个状态。 `produce` 将负责所有必要的复制,并通过冻结数据来防止未来的意外修改。 + +```javascript +import produce from "immer" + +const nextState = produce(baseState, draft => { + draft[1].done = true + draft.push({title: "Tweet about it"}) +}) +``` + +正在寻找结合 React 的 Immer?跳到 [React + Immer](example-setstate) 页面 + +### Immer 如何工作 + +基本思想是,使用 Immer,您会将所有更改应用到临时 _draft_,它是 _currentState_ 的代理。一旦你完成了所有的 _mutations_,Immer 将根据对 _draft state_ 的 _mutations_ 生成 nextState。这意味着您可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。 + +![immer-hd.png](/img/immer.png) + +使用 Immer 就像拥有一个私人助理。助手拿一封信(当前状态)并给您一份副本(草稿)以记录更改。完成后,助手将接受您的草稿并为您生成真正不变的最终字母(下一个状态)。 + +前往 [下一章节](./produce.mdx) 以进一步深入了解 `produce` + +## 好处 + +- 遵循不可变数据范式,同时使用普通的 JavaScript 对象、数组、Sets 和 Maps。无需学习新的 API 或 "mutations patterns"! +- 强类型,无基于字符串的路径选择器等 +- 开箱即用的结构共享 +- 开箱即用的对象冻结 +- 深度更新轻而易举 +- 样板代码减少。更少的噪音,更简洁的代码 +- 对 JSON 补丁的一流支持 +- 小:3KB gzip diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/map-set.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/map-set.md new file mode 100644 index 00000000..3189756c --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/map-set.md @@ -0,0 +1,60 @@ +--- +id: map-set +title: Map 和 Set +--- + +
+
+
+ +_⚠ 从版本6开始,对 `Map` 和 `Set` 的支持必须在启动应用程序时通过显式调用 [`enableMapSet()`](./installation.mdx#pick-your-immer-version)来开启_ + +普通对象、数组、`Map` 和 `Set` 总是可以用 Immer 更新。一个使用 `Map` 和 Immer 的示例: + +```javascript +test("Producers can update Maps", () => { + const usersById_v1 = new Map() + + const usersById_v2 = produce(usersById_v1, draft => { + // 修改 map 会生成一个新的 map + draft.set("michel", {name: "Michel Weststrate", country: "NL"}) + }) + + const usersById_v3 = produce(usersById_v2, draft => { + // 在 map 深处进行修改,同样会生成一个新的 map! + draft.get("michel").country = "UK" + }) + + // 我们每次都会得到一个新的 map + expect(usersById_v2).not.toBe(usersById_v1) + expect(usersById_v3).not.toBe(usersById_v2) + // 显然它们的内容不同 + expect(usersById_v1).toMatchInlineSnapshot(`Map {}`) + expect(usersById_v2).toMatchInlineSnapshot(` + Map { + "michel" => Object { + "country": "NL", + "name": "Michel Weststrate", + }, + } + `) + expect(usersById_v3).toMatchInlineSnapshot(` + Map { + "michel" => Object { + "country": "UK", + "name": "Michel Weststrate", + }, + } + `) + // 旧的从来不会被更改 + expect(usersById_v1.size).toBe(0) + // 试图在 produce 之外修改 map 对象是不行的! + expect(() => usersById_v3.clear()).toThrowErrorMatchingInlineSnapshot( + `"This object has been frozen and should not be mutated"` + ) +}) +``` + +Immer 生成的 Map 和 Set 将被人为地设置为不可变。这意味着在 `produce` 之外尝试 `set`、`clear`等可变方法时,它们将抛出异常。 + +注意:map 的**键**永远不会被更改!这样做是为了避免混淆语义并保持键始终引用相等 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/original.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/original.md new file mode 100644 index 00000000..0ca8f0cb --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/original.md @@ -0,0 +1,36 @@ +--- +id: original +title: 从 draft 中提取原始 state +sidebar_label: Original +--- + +
+
+
+ +Immer中暴露了一个命名对象 `original`,将从 `produce` 内部的代理实例获取原始对象(对于未代理值返回 `undefined`。 +一个好的例子是:当在一个树状 state 中使用严格相等搜索结点的时候它很有用。 + +```js +import {original, produce} from "immer" + +const baseState = {users: [{name: "Richie"}]} +const nextState = produce(baseState, draftState => { + original(draftState.users) // is === baseState.users +}) +``` + +只是想知道一个值是否是代理实例?使用 `isDraft` 函数!请注意,不能在不是 draft 的对象上调用 `original`。 + + +```js +import {isDraft, produce} from "immer" + +const baseState = {users: [{name: "Bobby"}]} +const nextState = produce(baseState, draft => { + isDraft(draft) // => true + isDraft(draft.users) // => true + isDraft(draft.users[0]) // => true +}) +isDraft(nextState) // => false +``` diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/patches.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/patches.mdx new file mode 100644 index 00000000..317e52e5 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/patches.mdx @@ -0,0 +1,230 @@ +--- +id: patches +title: Patches +--- + +
+
+
+
+ + egghead.io 第14课: 使用 produceWithPatches 捕获 Patches + +
+
+ +
+ + Hosted on egghead.io + +
+
+ + egghead.io 第16课: 使用 applyPatches 应用 Patches + +
+
+ +
+ + Hosted on egghead.io + +
+ +_⚠ 在版本6之后,必须在启动应用程序调用一次 [`enablePatches()`](./installation.mdx#pick-your-immer-version) 来启用对 Patches 的支持。_ + +在 producer 运行期间,Immer 可以记录所有的补丁来回溯 reducer造成的更改 。这是一个非常强大的工具,如果您想暂时 fork 您的状态并回溯对原始状态的更改。 + +Patches 在下面场景很有用: + +- 与其他方交换增量更新,例如通过 +- 对于调试/跟踪,准确查看状态如何随时间变化 +- 作为撤消/重做的基础或作为在稍微不同的状态树上回溯更改的方法。 + +为了帮助回溯补丁,`applyPatches` 派上用场了。这是一个如何使用 Patches 来记录增量更新并(反向)应用它们的示例: + +```javascript +import produce, {applyPatches} from "immer" + +// 版本 6 +import {enablePatches} from "immer" +enablePatches() + +let state = { + name: "Micheal", + age: 32 +} + +// 假设用户在向导中 +// 他的更改 应该以最终是否为基本状态结束... + +his changes should end up in the base state ultimately or not... +let fork = state +// 用户在向导中所作的所有更改 +let changes = [] +// 与向导中所做的所有更改相反 +let inverseChanges = [] + +fork = produce( + fork, + draft => { + draft.age = 33 + }, + // 产生的第三个参数是一个回调,patches 将从这里产生 + (patches, inversePatches) => { + changes.push(...patches) + inverseChanges.push(...inversePatches) + } +) + +// 同时,我们的原始状态被替换,例如 +// 从服务器收到了一些更改 +state = produce(state, draft => { + draft.name = "Michel" +}) + +// 当向导完成(成功)后,我们可以将 fork 中的更改重播到新的状态! +state = applyPatches(state, changes) + +// state 现在包含来自两个代码路径的更改! +expect(state).toEqual({ + name: "Michel", // 服务器更改 + age: 33 // 向导更改 +}) + +// 最后,即使在完成向导之后,用户也可能会改变主意并撤消他的更改...... +state = applyPatches(state, inverseChanges) +expect(state).toEqual({ + name: "Michel", // 没有还原 + age: 32 // 还原了 +}) +``` + +生成的 patches 与 [RFC-6902 JSON patch standard](https://datatracker.ietf.org/doc/html/rfc6902/#section-4.1) 类似(但并不相同),除了 path 属性是一个数组,而不是一个字符串。这使得处理 patches 更加容易。如果你想规范化到官方格式,`patch.path = patch.path.join("/")`应该可以解决这个问题。无论如何,下面就是一堆 patches 和它们回溯的样子。 + +```json +[ + { + "op": "replace", + "path": ["profile"], + "value": {"name": "Veria", "age": 5} + }, + {"op": "remove", "path": ["tags", 3]} +] +``` + +```json +[ + {"op": "replace", "path": ["profile"], "value": {"name": "Noa", "age": 6}}, + {"op": "add", "path": ["tags", 3], "value": "kiddo"} +] +``` + +⚠ 注意: Immer 生成的补丁集应该是正确的,也就是说,将它们应用于相同的基础对象应该会导致相同的最终状态。然而,Immer 不保证生成的补丁集是最优的,即可能的最小补丁集。它通常取决于被认为是“最佳”的用例,并且生成最佳补丁集在计算上可能非常昂贵。因此,在某些情况下,您可能想要对生成的补丁进行后处理,或者按照下面的说明压缩它们。 + +### `produceWithPatches` + +
+ + egghead.io 第19课: 使用回溯 patches 构建撤销功能 + +
+
+ +
+ + Hosted on egghead.io + +
+
+ + egghead.io 第20课: 使用 patches 构建重做功能 + +
+
+ +
+ + Hosted on egghead.io + +
+ +除了设置 patch 监听器之外,获取 patches 的更简单方法是使用 `produceWithPatches`,它与 `produce` 具有相同的签名,不过它 +不止返回 next state,而是一个包含 `[nextState, patches, inversePatches]` 的元组,和 `produce` 一样,`produceWithPatches` 也支持柯里化。 + +```javascript +import {produceWithPatches} from "immer" + +const [nextState, patches, inversePatches] = produceWithPatches( + { + age: 33 + }, + draft => { + draft.age++ + } +) +``` + +将返回: + +```javascript +;[ + { + age: 34 + }, + [ + { + op: "replace", + path: ["age"], + value: 34 + } + ], + [ + { + op: "replace", + path: ["age"], + value: 33 + } + ] +] +``` + +有关更深入的研究,请参阅使用 [Distributing patches and rebasing actions using Immer](https://medium.com/@mweststrate/distributing-state-changes-using-snapshots-patches-and-actions-part-2-2f50d8363988) + +提示:使用此技巧可以 [compress patches](https://medium.com/@david.b.edelstein/using-immer-to-compress-immer-patches-f382835b6c69) diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/performance.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/performance.mdx new file mode 100644 index 00000000..b28b3bc8 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/performance.mdx @@ -0,0 +1,83 @@ +--- +id: performance +title: Immer 性能 +--- + +
+
+
+ + egghead.io 第5课: 在 React 中利用 Immer 的结构共享 + +
+
+ +
+ + Hosted on egghead.io + +
+ + egghead.io 第7课: 如果没有语义变化,Immer 会使用原先的数据 + +
+
+ +
+ + Hosted on egghead.io + +
+ +这是一个关于 Immer 性能的 [简单 benchmark](https://github.com/immerjs/immer/blob/master/__performance_tests__/todo.js) 。该测试需要 50,000 个待办事项并更新其中的 5,000 个。 _Freeze_ 表示状态树在生成后已被冻结。这是一种开发最佳实践,因为它可以防止开发人员意外修改状态树。 + +上面的数字没有反映一些东西,但实际上,Immer 有时比手写的 reducer _快_ 得多。这样做的原因是,Immer 会检测“无操作”状态变化,如果实际上没有任何变化,则返回原始状态,这可以避免很多重新渲染。众所周知,只需应用 immer 即可解决关键性能问题。 + +这些测试在 Node 10.16.3 上执行。使用 `yarn test:perf` 在本地重现它们。 + +![performance.png](/img/performance.png) + +最重要的结论: + +- Immer with proxies 大约比手写 reducer 慢 2 到 3 倍(上面的测试用例是最坏的情况,请参阅 `yarn test:perf` 了解更多测试情况)。这在实践中可以忽略不计。 +- Immer 的速度大致与 ImmutableJS 一样快。但是,_immutableJS + toJS_ 明确了后期往往需要付出的代价;将 immutableJS 对象转换回普通对象,以便将它们传递给组件或者进行序列化操作在网络中传输......(还有将从服务器接收到的数据转换为不可变 JS 的前期成本) +- 生成 patches 不会显著减慢 immer +- ES5 后备实现的速度大约是代理实现的两倍,在某些情况下更糟。 + +## 性能提示 + +### 预冻结数据 + +当向 Immer producer 中的状态树添加大型数据集时(例如从 JSON 端点接收的数据),可以在首先添加的数据的根上调用 `freeze(json)` ,来_浅冻结_它。这将允许 Immer 更快地将新数据添加到树中,因为它将避免_递归_扫描和冻结新数据的需要。 + +### 您可以随时选择退出 + +immer 在任何地方都是可选的,因此手动编写性能非常苛刻的 reducers ,并将 immer 用于所有普通的的 reducers 是非常好的。即使在 producer 内部,您也可以通过使用 `original` 或 `current` 函数来选择退出 Immer 的某些部分逻辑,并对纯 JavaScript 对象执行一些操作。 + +### 对于昂贵的搜索操作,从原始 state 读取,而不是 draft + +Immer 会将您在 draft 中读取的任何内容也递归地转换为 draft。如果您对涉及大量读取操作的 draft 进行昂贵的无副作用操作,例如在非常大的数组中使用 `find(Index)` 查找索引,您可以通过首先进行搜索,并且只在知道索引后调用 `produce` 来加快速度。这样可以阻止 Immer 将在 draft 中搜索到的所有内容都进行转换。或者,使用 `original(someDraft)` 对 draft 的原始值执行搜索,这归结为同样的事情。 + +### 将 produce 拉到尽可能远的地方 + +始终尝试将 produce “向上”拉动,例如 `for (let x of y) produce(base, d => d.push(x))` 比 `produce(base, d => { for (let x of y) ) d.push(x)})` 慢得多 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/pitfalls.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/pitfalls.md new file mode 100644 index 00000000..751473ee --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/pitfalls.md @@ -0,0 +1,90 @@ +--- +id: pitfalls +title: 陷阱 +--- + +
+
+
+ +### 性能提示 + +对于性能提示,阅读 [性能提示](./performance.mdx#performance-tips). + +### 不要重新分配 recipe 参数 + +永远不要重新分配 `draft` 参数(例如:`draft = myCoolNewState`)。相反,要么修改 draft,要么返回新状态。请参阅[从 producers 返回数据](./return.mdx)。 + +### Immer 只支持单向树 + +Immer 假设您的状态是单向树。也就是说,任何对象都不应该在树中出现两次,也不应该有循环引用。从根到树的任何节点应该只有一条路径。 + +### 永远不要从 producer 那里显式返回 `undefined` + +可以从 producers 返回值,但不能以这种方式返回 `undefined`,因为它与根本不更新 draft 没有区别!如果你想用 `undefined` 替换 draft,只需从 producer 那里返回 `nothing`。 + +### 不要修改特殊对象 + +Immer [不支持特殊对象](https://github.com/immerjs/immer/issues/504) 比如 window.location. + +### 类应该是可 draft 的或不可变的 + +您将需要使自己的类能与 Immer 一起正常工作。有关该主题的文档,请查看有关使用[复杂对象](./complex-objects.md)的部分。 + +### 只有有效的索引和长度可以在数组上改变 + +对于数组,只能改变数值属性和 `length` 属性。自定义属性不会保留在数组上。 + +### 只有来自 state 的数据会被 draft + +请注意,来自闭包而不是来自基本 state 的数据将永远不会被 draft,即使数据已成为新 darft 的一部分。 + +```javascript +function onReceiveTodo(todo) { + const nextTodos = produce(todos, draft => { + draft.todos[todo.id] = todo + // 注意,因为 todo 来自外部,而不是 draft,所以他不会被 draft, + // 所以下面的修改会影响原来的 todo! + draft.todos[todo.id].done = true + + // 上面的代码相当于 + todo.done = true + draft.todos[todo.id] = todo + }) +} +``` + +### Immer patches 不一定是最优的 + +Immer 生成的 patches 应该是正确的,也就是说,将它们应用于相同的基础对象应该会导致相同的最终状态。然而,Immer 不保证生成的 patches 是最优的,即可能的最小 patches +### 始终使用嵌套 producers 的结果 + +支持嵌套调用 `produce` ,但请注意 `produce` 将_始终_产生新状态,因此即使将 draft 传递给嵌套 produce,内部 produce 所做的更改也不会在传递给它的 draft 中可见,只会反映在产生的输出中。换句话说,当使用嵌套 produce 时,您会得到 draft 的 draft,并且内部 produce 的结果应该合并回原始 draft(或返回)。例如,如果内部 produce 的输出没有被使用的话, `produce(state, draft => {produce(draft.user, userDraft => { userDraft.name += "!" })})` 将不会生效。使用嵌套 producers 的正确方法是: + + +```javascript +produce(state, draft => { + draft.user = produce(draft.user, userDraft => { + userDraft.name += "!" + }) +}) +``` + +### Drafts 在引用上不相等 + +Immer 中的 draft 对象包装在 `Proxy` 中,因此您不能使用 `==` 或 `===` 来测试原始对象与其 draft 之间的相等性(例如,当匹配数组中的特定元素时)。相反,您可以使用 `original` 助手: + + +```javascript +const remove = produce((list, element) => { + const index = list.indexOf(element) // 不会工作! + const index = original(list).indexOf(element) // 用这个! + if (index > -1) list.splice(index, 1) +}) + +const values = [a, b, c] +remove(values, a) +``` + +如果可以的话,建议在 `produce` 函数之外执行比较,或者使用 `.id` 之类的唯一标识符属性,以避免需要使用 `original`。 + diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/produce.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/produce.mdx new file mode 100644 index 00000000..fe7fa051 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/produce.mdx @@ -0,0 +1,91 @@ +--- +id: produce +title: 使用 produce +--- + +
+
+
+
+ + egghead.io 第 3 课:使用 produce 简化深度更新 + +
+
+ +
+ + Hosted on egghead.io + +
+ +Immer 包暴露了一个完成所有工作的默认函数。 + +`produce(currentState, recipe: (draftState) => void): nextState` + +`produce` 需要一个 `baseState`,以及一个可用于对传入的 `draft` 进行所有所需更改的 `recipe`。关于 Immer 的有趣之处在于 baseState 将保持不变,但 nextState 将反映对 DraftState 所做的所有更改. + +在 `recipe` 中,所有标准的 JavaScript API 都可以在 `draft` 对象上使用,包括属性字段分配、删除操作和修改数组、Map 和 Set 操作,如 push、pop、splice、set、sort、remove 等。 + +这些 `mutations` 中的任何一个都不必发生在初始对象上,但它可以修改 `draft` 深处的任何内容:`draft.todos[0].tags["urgent"].author.age = 56` + +请注意,`recipe` 函数通常不会返回任何内容。但是,如果您想用另一个对象完全替换 `draft`,则可以返回,有关更多详细信息,请参阅返回[新数据](return)。 + +## 例子 + +```javascript +import produce from "immer" + +const baseState = [ + { + title: "Learn TypeScript", + done: true + }, + { + title: "Try Immer", + done: false + } +] + +const nextState = produce(baseState, draftState => { + draftState.push({title: "Tweet about it"}) + draftState[1].done = true +}) +``` + +```javascript +// 新的 item 仅仅被添加到了 next state +// base state 没有被修改 +expect(baseState.length).toBe(2) +expect(nextState.length).toBe(3) + +// 同上 +expect(baseState[1].done).toBe(false) +expect(nextState[1].done).toBe(true) + +// 未修改的数据结构共享 +expect(nextState[0]).toBe(baseState[0]) +// 改变的数据不是 +expect(nextState[1]).not.toBe(baseState[1]) +``` + +### 术语 + +- `(base)state`, 传递给 `produce` 的不可变状态 +- `recipe`: `produce` 的第二个参数,它捕获了 base state 应该如何 `mutated`。 +- `draft`: 任何 `recipe` 的第一个参数,它是可以安全 `mutate` 的原始状态的代理。 +- `producer`. 一个使用 `produce` 的函数,通常形式为 `(baseState, ...arguments) => resultState` + +请注意,命名 `recipe` 的第一个参数 `draft` 并不是绝对必要的。您可以将其命名为任何您想要的名称,例如 `user`。使用 `draft` 作为名称只是一个约定,以表明:“这里的 `mutation` 是可以的”。 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/resources.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/resources.md new file mode 100644 index 00000000..bf5fde0a --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/resources.md @@ -0,0 +1,18 @@ +--- +id: resources +title: 外部资源 +--- + +
+
+
+ +- 博客: [Immer 在 React 中的兴起](https://www.netlify.com/blog/2018/09/12/the-rise-of-immer-in-react/) +- 博客: 由 Workday Prism 解释他们为什么选择 Immer 来管理不可变状态 [寻找强类型、不可变的状态](https://medium.com/workday-engineering/workday-prism-analytics-the-search-for-a-strongly-typed-immutable-state-a09f6768b2b5) +- 博客: [React 和 Redux 中的不可变性:完整指南](https://daveceddia.com/react-redux-immutability-guide/) +- 视频教程: [将 Immer 与 React.setState 一起使用](https://codedaily.io/screencasts/86/Immutable-Data-with-Immer-and-React-setState) +- Michel Weststrate 在 React Finland 2018 上的 Immer [演讲](https://www.youtube.com/watch?v=-gJbS7YjcSo) + [幻灯片](http://immer.surge.sh/) +- [ForwardJS 2019:不可变性正在改变——从 Immutable.js 到 Immer](https://www.youtube.com/watch?v=bFuRvcAEiHg&feature=youtu.be) by [shawn swyx wang](https://twitter.com/swyx/) +- [演讲:Immer、不可变性和代理的奇妙世界](https://www.youtube.com/watch?v=4Nb9Gwp2L24) + [幻灯片](https://jsnation-proxies.surge.sh/), JSNation 2019, Michel Weststrate +- 博客: [使用快照、patches 和操作分发状态更改](https://medium.com/@mweststrate/distributing-state-changes-using-snapshots-patches-and-actions-part-1-2811a2fcd65f) +- 博客: [在 Redux 中实现 Undo-Redo 功能](https://techinscribed.com/implementing-undo-redo-functionality-in-redux-using-immer/), 2019年9月 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/return.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/return.mdx new file mode 100644 index 00000000..4b557986 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/return.mdx @@ -0,0 +1,152 @@ +--- +id: return +title: 从 producers 返回新数据 +--- + +
+
+
+
+ + egghead.io 第9课: 返回全新 state + +
+
+ +
+ + Hosted on egghead.io + +
+ +不需要从 producer 那里返回任何东西,因为 Immer 无论如何都会返回 `draft` 的(最终)版本。但是,也允许仅仅 `return draft`。 + +也允许从 producer 函数中任意返回其他数据。但_前提_是你没有修改 `draft`。这对于产生一个全新的 state 很有用。一些例子: + +```javascript +const userReducer = produce((draft, action) => { + switch (action.type) { + case "renameUser": + //可以: 我们修改了当前的 state + draft.users[action.payload.id].name = action.payload.name + return draft // 与仅仅 'return' 相同 + case "loadUsers": + // 可以: 我们返回了一个全新的 state + return action.payload + case "adduser-1": + // 不行: 这不会改变 draft ,也不会返回新的状态 + // 它不会修改 draft(它只是重新声明它) + // 事实上,这根本没有做任何事情 + draft = {users: [...draft.users, action.payload]} + return + case "adduser-2": + // 不行: 修改 draft 的同时返回了一个新的状态 + draft.userCount += 1 + return {users: [...draft.users, action.payload]} + case "adduser-3": + // 可以: 返回一个新的状态。但是,不必要的复杂和昂贵 + return { + userCount: draft.userCount + 1, + users: [...draft.users, action.payload] + } + case "adduser-4": + // 可以: immer 的方式 + draft.userCount += 1 + draft.users.push(action.payload) + return + } +}) +``` + +_注意:无法以这种方式返回 `undefined` ,因为它与不更新 draft 没有区别!继续阅读......_ + +## 使用 `nothing` 产生 `undefined` + +因此,一般来说,可以通过从 producer 返回一个新值来替换当前 state,而不是修改 draft。然而,有一个微妙的边缘情况:如果您尝试编写一个想要用 undefined 替换当前状态的 producer: + +```javascript +produce({}, draft => { + // 什么也不干 +}) +``` + +或者: + +```javascript +produce({}, draft => { + // 尝试从 producer 中返回 undefined + return undefined +}) +``` + +问题在于,在 JavaScript 中,一个不返回任何内容的函数也会返回 `undefined`!所以 immer 无法区分这些不同的情况。因此,默认情况下,Immer 会假设任何返回 `undefined` 的 producer 只是试图修改 draft。 + +但是,为了让 Immer 清楚您有意生成 `undefined` 值,您可以返回内置标记 `nothing`: + +```javascript +import produce, {nothing} from "immer" + +const state = { + hello: "world" +} + +produce(state, draft => {}) +produce(state, draft => undefined) +// 都会返回最初的状态: { hello: "world"} + +produce(state, draft => nothing) +// 产生一个新的状态, 'undefined' +``` + +注:请注意,此问题特定于 `undefined` 值,任何其他值(包括 `null`)都不会受到此问题的影响 + +提示:为了能够在使用 TypeScript 时从 recipe 中返回 `nothing`,`state` 的类型必须接受 undefined 值。 + +## 使用 `void` 的内联快捷方式 + +
+ + egghead.io 第10课: 使用 _void_ 避免意外的返回 + +
+
+ +
+ + Hosted on egghead.io + +
+ +Immer 中的 draft 修改通常需要一段代码块,因为返回表示覆盖。有时候你可能觉得这么多的样板代码很糟心。 + +在这种情况下,您可以使用 javascripts [`void`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void) 运算符,它计算表达式并返回 `undefined`。 + +```javascript +// 单次修改 +produce(draft => void (draft.user.age += 1)) + +// 多次修改 +produce(draft => void ((draft.user.age += 1), (draft.user.height = 186))) +``` + +代码风格是高度个人化的,但对于要被许多人理解的代码库,我们建议坚持经典的 `draft => { draft.user.age += 1}` 以避免认知开销。 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/support.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/support.md new file mode 100644 index 00000000..2df46637 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/support.md @@ -0,0 +1,13 @@ +--- +id: support +title: 赞助 immer +--- + +
+
+
+ +Immer 目前在 GitHub 上有 350000 个项目依赖于它,每月的下载量接近 10000000 次。然而,只有一开始两天的开发得到了赞助(由 Mendix),之后的所有开发和维护都是用爱发电。 + +如果您喜欢 Immer,并且对这个包心存感激,或者想确保它的使用寿命,请考虑在 https://opencollective.com/immer 上赞助或使用 [PayPal](https://www.paypal.me/michelweststrate) 进行一次性捐赠。 + diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/typescript.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/typescript.mdx new file mode 100644 index 00000000..cec0f19b --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/typescript.mdx @@ -0,0 +1,210 @@ +--- +id: typescript +title: Using TypeScript or Flow +sidebar_label: TypeScript / Flow +--- + +
+
+
+ + egghead.io 第12课: Immer + TypeScript + +
+
+ +
+ + Hosted on egghead.io + +
+ +Immer 包附带了类型定义,TypeScript 和 Flow 开箱即可获取这些定义,无需进一步配置 + +TypeScript 类型会自动从 draft 类型中删除 `readonly` 修饰符,并返回与原始类型匹配的值。看这个实际的例子: + +```ts +import produce from "immer" + +interface State { + readonly x: number +} + +// `x` 不能被修改 +const state: State = { + x: 0 +} + +const newState = produce(state, draft => { + // `x` 可以被修改 + draft.x++ +}) + +// `newState.x` 不能在这里被修改 +``` +这确保了您可以修改状态的唯一位置是在您的 produce 回调中。它甚至可以递归地和 `ReadonlyArray` 一起工作。 + +## 最佳实践 + +1. 始终尽可能将您的 state 定义为只读。这最好地反映了心智模型和现实,因为 Immer 将冻结其所有返回值。 +2. 您可以使用实用类型 `Immutable` 递归地使整个类型树成为只读的,例如:`type ReadonlyState = Immutable` +3. 如果输入状态的原始类型不是不可变的,则 Immer 不会自动将所有返回的类型包装在 `Immutable` 中。这是为了确保它不会破坏不使用不可变类型的代码库。 + +## 柯里化 producers 的提示 + +我们尝试尽可能多地推断。因此,如果创建了一个柯里化 producer 并直接传递给另一个函数,我们可以从那里推断出类型。这适用于例如 React: +```typescript +import {Immutable, produce} from "immer" + +type Todo = Immutable<{ + title: string + done: boolean +}> + +// 然后... + +const [todo, setTodo] = useState({ + title: "test", + done: true +}) + +// 然后... + +setTodo( + produce(draft => { + // draft 将是强类型和可变的! + draft.done = !draft.done + }) +) +``` + +当柯里化 producers 没有直接传递到其他地方时,Immer 可以从 draft 参数推断状态类型。例如在执行以下操作时: + +```typescript +// 请参阅下文以获得更好的解决方案 + +const toggler = produce((draft: Draft) => { + draft.done = !draft.done +}) + +// typeof toggler = (state: Immutable) => Writable +``` + +请注意,我们确实用 `Draft` 包装了 `draft` 参数的 `Todo` 类型,因为 `Todo` 是只读类型。对于非只读类型,这不是必需的 + +对于返回的柯里化函数 `toggler`,我们将输入类型缩小为 `Immutable`,这样即使 `Todo` 是可变类型,我们仍将接受不可变的 todo 作为切换器的输入参数。 + +与之相反,Immer 会将柯里化函数的输出类型_扩展_为 `Writable`,以确保它的输出状态也可分配给未明确键入为不可变的变量。 + +这种类型的缩小/扩大行为可能不受欢迎,甚至可能因为它会导致类型非常多的噪音。因此,我们建议为柯里化 produces 指定 state 泛型 ,以防它无法直接推断,例如上面的 `toggler`。通过这样做,将跳过自动输出扩大/输入缩小。然而,`draft` 参数本身仍将被推断为可写 `Draft`: + + +```typescript +const toggler = produce(draft => { + draft.done = !draft.done +}) + +// typeof toggler = (state: Todo) => Todo +``` + +但是,如果柯里化 producer 定义了初始状态,Immer 可以从初始状态推断状态类型,因此在这种情况下也不需要指定泛型: + +```typescript +const state0: Todo = { + title: "test", + done: false +} + +// 不需要类型注释,因为我们可以从 state0 推断。 +const toggler = produce(draft => { + draft.done = !draft.done +}, state0) + +// typeof toggler = (state: Todo) => Todo +``` + +如果 toggler 没有初始状态,并且它有柯里化参数,并且您显式设置 state 泛型,则任何附加参数的类型也应显式定义为元组类型: + +```typescript +const toggler = produce((draft, newState) => { + draft.done = newState +}) + +// typeof toggler = (state: Todo, newState: boolean) => Todo +``` + +## 类型转换 + +`produce` 内部和外部的类型在概念上可以相同,但从实际角度来看是不同的。例如,上面示例中的 `State` 应被视为在 `produce` 外部不可变,但在 `produce` 内部是可变的。 + +有时这会导致实际冲突。举个例子: + +```typescript +type Todo = {readonly done: boolean} + +type State = { + readonly finishedTodos: readonly Todo[] + readonly unfinishedTodos: readonly Todo[] +} + +function markAllFinished(state: State) { + produce(state, draft => { + draft.finishedTodos = state.unfinishedTodos + }) +} +``` + +这将产生错误: + +``` +The type 'readonly Todo[]' is 'readonly' and cannot be assigned to the mutable type '{ done: boolean; }[]' +``` + +这个错误的原因是我们将只读的、不可变的数组分配给我们的 draft,draft 需要一个可变的类型,并带有 .push 等方法。就 TS 而言,这些并没有从我们的原始 `State` 中暴露出来。为了提示 TypeScript 我们希望将此处的集合向上转换为可变数组以用于 draft,我们可以使用函数 `castDraft`: + + +`draft.finishedTodos = castDraft(state.unfinishedTodos)` 将使错误消失。 + +还有函数 `castImmutable`,以防您需要实现相反的效果。请注意,这些函数出于所有实际目的都是无操作的,它们只会返回其原始值。 + +提示:您可以将 `castImmutable` 与 `produce` 结合起来,将 `produce` 的返回类型定义为不可变的内容,即使原始 state 是可变的 + +```typescript +// 一个可变数据结构 +const baseState = { + todos: [{ + done: false + }] +} + +const nextState = castImmutable(produce(baseState, _draft => {})) + +// nextState 的推断类型现在是: +{ + readonly todos: ReadonlyArray<{ + readonly done: boolean + }> +}) +``` + +## 兼容性 + +**注意:** Immer v5.3+ 仅支持 TypeScript v3.7+ + +**注意:** Immer v3.0+ 仅支持 TypeScript v3.4+ + +**注意:** Immer v1.9+ 仅支持 TypeScript v3.1+ + +**注意:** 在未来的版本中可能会删除 flow 支持,我们建议使用 TypeScript diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/update-patterns.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/update-patterns.md new file mode 100644 index 00000000..7834425f --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/update-patterns.md @@ -0,0 +1,142 @@ +--- +id: update-patterns +title: 更新模式 +--- + +
+
+
+ +在 Immer 之前,使用不可变数据意味着学习所有不可变的更新模式。 + +为了帮助“忘记”这些模式,这里概述了如何利用内置 JavaScript API 来更新对象和集合 + +### 更新对象 + +```javascript +import produce from "immer" + +const todosObj = { + id1: {done: false, body: "Take out the trash"}, + id2: {done: false, body: "Check Email"} +} + +// 添加 +const addedTodosObj = produce(todosObj, draft => { + draft["id3"] = {done: false, body: "Buy bananas"} +}) + +// 删除 +const deletedTodosObj = produce(todosObj, draft => { + delete draft["id1"] +}) + +// 更新 +const updatedTodosObj = produce(todosObj, draft => { + draft["id1"].done = true +}) +``` + +### 更新数组 + +```javascript +import produce from "immer" + +const todosArray = [ + {id: "id1", done: false, body: "Take out the trash"}, + {id: "id2", done: false, body: "Check Email"} +] + +// 添加 +const addedTodosArray = produce(todosArray, draft => { + draft.push({id: "id3", done: false, body: "Buy bananas"}) +}) + +// 索引删除 +const deletedTodosArray = produce(todosArray, draft => { + draft.splice(3 /*索引 */, 1) +}) + +// 索引更新 +const updatedTodosArray = produce(todosArray, draft => { + draft[3].done = true +}) + +// 索引插入 +const updatedTodosArray = produce(todosArray, draft => { + draft.splice(3, 0, {id: "id3", done: false, body: "Buy bananas"}) +}) + +// 删除最后一个元素 +const updatedTodosArray = produce(todosArray, draft => { + draft.pop() +}) + +// 删除第一个元素 +const updatedTodosArray = produce(todosArray, draft => { + draft.shift() +}) + +// 数组开头添加元素 +const addedTodosArray = produce(todosArray, draft => { + draft.unshift({id: "id3", done: false, body: "Buy bananas"}) +}) + +// 根据 id 删除 +const deletedTodosArray = produce(todosArray, draft => { + const index = draft.findIndex(todo => todo.id === "id1") + if (index !== -1) draft.splice(index, 1) +}) + +// 根据 id 更新 +const updatedTodosArray = produce(todosArray, draft => { + const index = draft.findIndex(todo => todo.id === "id1") + if (index !== -1) draft[index].done = true +}) + +// 过滤 +const updatedTodosArray = produce(todosArray, draft => { + // 过滤器实际上会返回一个不可变的状态,但是如果过滤器不是处于对象的顶层,这个依然很有用 + return draft.filter(todo => todo.done) +}) +``` + +### 嵌套数据结构 + +```javascript +import produce from "immer" + +// 复杂数据结构例子 +const store = { + users: new Map([ + [ + "17", + { + name: "Michel", + todos: [ + { + title: "Get coffee", + done: false + } + ] + } + ] + ]) +} + +// 深度更新 +const nextStore = produce(store, draft => { + draft.users.get("17").todos[0].done = true +}) + +// 过滤 +const nextStore = produce(store, draft => { + const user = draft.users.get("17") + + user.todos = user.todos.filter(todo => todo.done) +}) +``` + +请注意,许多数组操作可用于通过传递多个参数或使用展开操作来一次插入多个元素:`todos.unshift(...items)`。 + +请注意,当处理包含通常由某个 id 标识的对象的数组时,我们建议使用基于 `Map` 或索引的对象(如上所示)而不是执行频繁的查找操作,查找表通常执行效率更高。 diff --git a/website/i18n/zh-CN/docusaurus-theme-classic/footer.json b/website/i18n/zh-CN/docusaurus-theme-classic/footer.json new file mode 100644 index 00000000..99363065 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-theme-classic/footer.json @@ -0,0 +1,6 @@ +{ + "copyright": { + "message": "Copyright © 2022 Michel Weststrate", + "description": "The footer copyright" + } +} \ No newline at end of file diff --git a/website/i18n/zh-CN/docusaurus-theme-classic/navbar.json b/website/i18n/zh-CN/docusaurus-theme-classic/navbar.json new file mode 100644 index 00000000..5f712e40 --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-theme-classic/navbar.json @@ -0,0 +1,18 @@ +{ + "title": { + "message": "Immer", + "description": "The title in the navbar" + }, + "item.label.Documentation": { + "message": "文档", + "description": "Navbar item with label Documentation" + }, + "item.label.GitHub": { + "message": "GitHub", + "description": "Navbar item with label GitHub" + }, + "item.label.Support Immer": { + "message": "赞助 Immer", + "description": "Navbar item with label Support Immer" + } +} \ No newline at end of file diff --git a/website/src/css/immer-infima.css b/website/src/css/immer-infima.css index dfa41d6e..6e245808 100644 --- a/website/src/css/immer-infima.css +++ b/website/src/css/immer-infima.css @@ -69,3 +69,7 @@ details a { iframe { border: none; } + +.dropdown__link { + color: black !important; +} From b2db62b75bcf172266493ffea15edd7125663cb7 Mon Sep 17 00:00:00 2001 From: Candid Dauth Date: Mon, 4 Apr 2022 17:33:13 +0200 Subject: [PATCH 3/9] feature: support importing ES module from Node.js (#901) (#921) --- package.json | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f0b5b963..7bca29ca 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,19 @@ "version": "9.0.0-beta.1", "description": "Create your next immutable state by mutating the current one", "main": "dist/index.js", - "module": "dist/immer.esm.js", + "module": "dist/immer.esm.mjs", + "exports": { + ".": { + "import": "./dist/immer.esm.mjs", + "require": "./dist/index.js" + }, + "./*": "./*" + }, "umd:main": "dist/immer.umd.production.min.js", "unpkg": "dist/immer.umd.production.min.js", "jsdelivr": "dist/immer.umd.production.min.js", - "jsnext:main": "dist/immer.esm.js", - "react-native": "dist/immer.esm.js", + "jsnext:main": "dist/immer.esm.mjs", + "react-native": "dist/immer.esm.mjs", "source": "src/immer.ts", "types": "./dist/immer.d.ts", "typesVersions": { @@ -32,7 +39,7 @@ "watch": "jest --watch", "coverage": "jest --coverage", "coveralls": "jest --coverage && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf ./coverage", - "build": "rimraf dist/ && tsdx build --name immer --format esm,cjs,umd && yarn build:flow", + "build": "rimraf dist/ && tsdx build --name immer --format esm,cjs,umd && cp dist/immer.esm.js dist/immer.esm.mjs && yarn build:flow", "build:flow": "cpx 'src/types/index.js.flow' dist -v", "publish-docs": "cd website && GIT_USER=mweststrate USE_SSH=true yarn docusaurus deploy", "start": "cd website && yarn start", From 0a47a4049d3c828547854bd814898f44b3fc9ae4 Mon Sep 17 00:00:00 2001 From: Peter Zich Date: Sat, 23 Apr 2022 08:39:32 -0700 Subject: [PATCH 4/9] docs: Fix link to `return` on `produce` doc page (#927) The current format was linking to https://immerjs.github.io/immer/produce/return , which does not exist --- website/docs/produce.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/produce.mdx b/website/docs/produce.mdx index 266bc500..62fbed85 100644 --- a/website/docs/produce.mdx +++ b/website/docs/produce.mdx @@ -40,7 +40,7 @@ Inside the recipe, all standard JavaScript APIs can be used on the `draft` objec Any of those mutations don't have to happen at the root, but it is allowed to modify anything anywhere deep inside the draft: `draft.todos[0].tags["urgent"].author.age = 56` -Note that the recipe function itself normally doesn't return anything. However, it is possible to return in case you want to replace the `draft` object in its entirety with another object, for more details see [returning new data](return). +Note that the recipe function itself normally doesn't return anything. However, it is possible to return in case you want to replace the `draft` object in its entirety with another object, for more details see [returning new data](./return.mdx). ## Example From fcb7a5590ce46d7dddee6f5382fb0f366370b95d Mon Sep 17 00:00:00 2001 From: V K <84337571+1e9y@users.noreply.github.com> Date: Sat, 23 Apr 2022 17:40:26 +0200 Subject: [PATCH 5/9] docs: Update produce/mdx (#926) `produce` function operates on `baseState` and produces `nextState`. Everywhere in this docs original state called `baseState`, but in the first snippet it's called `currentState`. I'm renaming it for consistency. --- website/docs/produce.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/produce.mdx b/website/docs/produce.mdx index 62fbed85..dfe238be 100644 --- a/website/docs/produce.mdx +++ b/website/docs/produce.mdx @@ -32,7 +32,7 @@ title: Using produce The Immer package exposes a default function that does all the work. -`produce(currentState, recipe: (draftState) => void): nextState` +`produce(baseState, recipe: (draftState) => void): nextState` `produce` takes a base state, and a _recipe_ that can be used to perform all the desired mutations on the `draft` that is passed in. The interesting thing about Immer is that the `baseState` will be untouched, but the `nextState` will reflect all changes made to `draftState`. From 442b6a05d95c0854711fc69beeabdf523ab8ff42 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Mon, 2 May 2022 11:27:06 +0100 Subject: [PATCH 6/9] docs: Add article that explains how immerhin is built on top of immer (#930) * add article that explains how immerhin is built on top of immer * Link to the author of immerhin article --- website/docs/resources.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/docs/resources.md b/website/docs/resources.md index a9c8f473..d6bea563 100644 --- a/website/docs/resources.md +++ b/website/docs/resources.md @@ -16,3 +16,4 @@ title: External resources - [Talk: Immer, Immutability and the Wonderful World of Proxies](https://www.youtube.com/watch?v=4Nb9Gwp2L24) + [slides](https://jsnation-proxies.surge.sh/), JSNation 2019, Michel Weststrate - Blog: [Distributing state changes using snapshots, patches and actions](https://medium.com/@mweststrate/distributing-state-changes-using-snapshots-patches-and-actions-part-1-2811a2fcd65f) - Blog: [Implementing Undo-Redo Functionality in Redux](https://techinscribed.com/implementing-undo-redo-functionality-in-redux-using-immer/), Sep 2019 +- Blog: [Synchronized immutable state with time travel](https://dev.to/oleg008/synchronized-immutable-state-with-time-travel-2c6o), Apr 2022, by [Oleg Isonen](https://twitter.com/oleg008) From 285fff927428291559505ec057512811c1951d10 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Mon, 2 May 2022 11:27:46 +0100 Subject: [PATCH 7/9] docs: add immerhin to built with (#929) --- website/docs/built-with.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/docs/built-with.md b/website/docs/built-with.md index 7c1dc566..2fc1fd1f 100644 --- a/website/docs/built-with.md +++ b/website/docs/built-with.md @@ -21,4 +21,5 @@ title: Built with Immer - [react-state-tree](https://github.com/suchipi/react-state-tree) _Drop-in replacement for useState that persists your state into a redux-like state tree_ - [redux-immer](https://github.com/salvoravida/redux-immer) _is used to create an equivalent function of Redux combineReducers that works with `immer` state. Like `redux-immutable` but for `immer`_ - [ngrx-wieder](https://github.com/nilsmehlhorn/ngrx-wieder) _Lightweight yet configurable solution for implementing undo-redo in Angular apps on top of NgRx and Immer_ +- [immerhin](https://github.com/webstudio-is/immerhin) Sync state with undo/redo - ... and [many more](https://www.npmjs.com/browse/depended/immer) From 9f7623d43466bd3b705099f0c825cb00670a9214 Mon Sep 17 00:00:00 2001 From: Myles J Date: Wed, 11 May 2022 18:41:28 +0100 Subject: [PATCH 8/9] fix: incorrect return type for async produceWithPatches (#933) --- src/types/types-external.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/types-external.ts b/src/types/types-external.ts index 08ccbea9..934a70e4 100644 --- a/src/types/types-external.ts +++ b/src/types/types-external.ts @@ -248,7 +248,7 @@ export interface IProduceWithPatches { base: Base, recipe: (draft: D) => Promise>, listener?: PatchListener - ): PatchesTuple> + ): Promise> } // Fixes #507: bili doesn't export the types of this file if there is no actual source in it.. From 220d61d47fea7fae24c794f14dc529bcd7b46249 Mon Sep 17 00:00:00 2001 From: Myles J Date: Wed, 11 May 2022 18:42:15 +0100 Subject: [PATCH 9/9] fix: consistent recipe return type in produceWithPatches (#934) --- src/types/types-external.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/types-external.ts b/src/types/types-external.ts index 934a70e4..d9cf27f3 100644 --- a/src/types/types-external.ts +++ b/src/types/types-external.ts @@ -241,12 +241,12 @@ export interface IProduceWithPatches { ): InferCurriedFromInitialStateAndRecipe >( base: Base, - recipe: (draft: D) => ValidRecipeReturnType, + recipe: (draft: D) => ValidRecipeReturnType, listener?: PatchListener ): PatchesTuple >( base: Base, - recipe: (draft: D) => Promise>, + recipe: (draft: D) => Promise>, listener?: PatchListener ): Promise> }