使用 Immer 的更好的减速器

已发表: 2022-03-10
快速总结↬在本文中,我们将学习如何使用 Immer 编写 reducer。 在使用 React 时,我们会维护很多状态。 为了更新我们的状态,我们需要编写大量的 reducer。 手动编写 reducer 会导致代码臃肿,我们必须触及状态的几乎每个部分。 这是乏味且容易出错的。 在本文中,我们将了解 Immer 如何为编写 state reducer 的过程带来更多的简单性。

作为一个 React 开发者,你应该已经熟悉了state 不应该直接改变的原则。 您可能想知道这意味着什么(我们大多数人在刚开始时都感到困惑)。

本教程将对此进行公正处理:您将了解什么是不可变状态以及对它的需求。 您还将学习如何使用 Immer 处理不可变状态以及使用它的好处。 您可以在此 Github 存储库中找到本文中的代码。

JavaScript 中的不变性及其重要性

Immer.js 是一个小型 JavaScript 库,由 Michel Weststrate 编写,其既定使命是让您“以更方便的方式处理不可变状态”。

但在深入研究 Immer 之前,让我们快速回顾一下 JavaScript 中的不变性以及它在 React 应用程序中的重要性。

最新的 ECMAScript(又名 JavaScript)标准定义了九种内置数据类型。 在这九种类型中,有六种被称为primitive值/类型。 这六个原语是undefinednumberstringbooleanbigintsymbol 。 使用 JavaScript 的typeof运算符进行简单检查将揭示这些数据类型的类型。

 console.log(typeof 5) // number console.log(typeof 'name') // string console.log(typeof (1 < 2)) // boolean console.log(typeof undefined) // undefined console.log(typeof Symbol('js')) // symbol console.log(typeof BigInt(900719925474)) // bigint

primitive是一个不是对象且没有方法的值。 对于我们目前的讨论来说,最重要的是一个原语的值一旦被创建就不能改变。 因此,原语被称为immutable的。

其余三种类型是nullobjectfunction 。 我们还可以使用typeof运算符检查它们的类型。

 console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function

这些类型是mutable的。 这意味着它们的值可以在创建后随时更改。

您可能想知道为什么我在上面有数组[0, 1] 。 好吧,在 JavaScriptland 中,数组只是一种特殊类型的对象。 如果您还想知道null以及它与undefined有何不同。 undefined只是意味着我们没有为变量设置值,而null是对象的特例。 如果您知道某物应该是一个对象,但该对象不存在,您只需返回null

为了用一个简单的例子来说明,试着在你的浏览器控制台中运行下面的代码。

 console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]

String.prototype.match应该返回一个数组,它是一个object类型。 当它找不到这样的对象时,它返回null 。 返回undefined在这里也没有意义。

够了。 让我们回到讨论不变性。

跳跃后更多! 继续往下看↓

根据 MDN 文档:

“除了对象之外的所有类型都定义了不可变的值(即不能更改的值)。”

该语句包含函数,因为它们是一种特殊类型的 JavaScript 对象。 请参阅此处的函数定义。

让我们快速了解一下可变和不可变数据类型在实践中的含义。 尝试在浏览器控制台中运行以下代码。

 let a = 5; let b = a console.log(`a: ${a}; b: ${b}`) // a: 5; b: 5 b = 7 console.log(`a: ${a}; b: ${b}`) // a: 5; b: 7

我们的结果表明,即使b是从a “派生”的,更改b的值也不会影响a的值。 这是因为当 JavaScript 引擎执行语句b = a时,它会创建一个新的、单独的内存位置,将5放入其中,并将b指向该位置。

对象呢? 考虑下面的代码。

 let c = { name: 'some name'} let d = c; console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"some name"}; d: {"name":"some name"} d.name = 'new name' console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"new name"}; d: {"name":"new name"}

我们可以看到,通过变量d更改 name 属性也会在c中更改它。 这是因为当 JavaScript 引擎执行语句c = { name: 'some name ' }时,JavaScript 引擎会在内存中创建一个空间,将对象放入其中,并将c指向它。 然后,当它执行语句d = c时,JavaScript 引擎只是将d指向同一个位置。 它不会创建新的内存位置。 因此,对d中的项目的任何更改都隐含地是对c中的项目的操作。 不费力气,我们就能明白为什么这是个麻烦事。

想象一下,您正在开发一个 React 应用程序,并且您想在某个地方通过读取变量c来将用户名显示为some name 。 但是在其他地方,您通过操作对象d在代码中引入了错误。 这将导致用户名显示为new name 。 如果cd是原语,我们就不会有这个问题。 但是对于典型的 React 应用程序必须维护的状态种类来说,原语太简单了。

这就是为什么在应用程序中保持不可变状态很重要的主要原因。 我鼓励您通过阅读 Immutable.js README 中的这个简短部分来检查其他一些注意事项:不变性的案例。

了解了为什么我们需要在 React 应用程序中保持不变性之后,现在让我们看看 Immer 如何通过其produce函数解决这个问题。

Immer的produce功能

Immer 的核心 API 非常小,您将使用的主要功能是produce功能。 produce简单地接受一个初始状态和一个定义状态应该如何变化的回调。 回调本身接收到它进行所有预期更新的状态的草稿(相同,但仍然是副本)副本。 最后,它produce一个新的、不可变的状态,并应用了所有的更改。

这种状态更新的一般模式是:

 // produce signature produce(state, callback) => nextState

让我们看看这在实践中是如何工作的。

 import produce from 'immer' const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], } // to add a new package const newPackage = { name: 'immer', installed: false } const nextState = produce(initState, draft => { draft.packages.push(newPackage) })

在上面的代码中,我们简单地传递了起始状态和一个回调来指定我们希望突变如何发生。 就这么简单。 我们不需要触及该州的任何其他部分。 它使initState保持不变,并在结构上共享我们在起始状态和新状态之间未触及的那些状态部分。 在我们的州,其中一个这样的部分是pets数组。 nextState produce一个不可变的状态树,其中包含我们所做的更改以及我们未修改的部分。

有了这些简单但有用的知识,让我们看看produce如何帮助我们简化 React reducer。

使用 Immer 编写减速器

假设我们有下面定义的状态对象

const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };

我们想添加一个新对象,然后在后续步骤中,将其installed的密钥设置为true

 const newPackage = { name: 'immer', installed: false };

如果我们使用 JavaScript 对象和数组扩展语法以通常的方式执行此操作,我们的状态缩减器可能如下所示。

 const updateReducer = (state = initState, action) => { switch (action.type) { case 'ADD_PACKAGE': return { ...state, packages: [...state.packages, action.package], }; case 'UPDATE_INSTALLED': return { ...state, packages: state.packages.map(pack => pack.name === action.name ? { ...pack, installed: action.installed } : pack ), }; default: return state; } };

我们可以看到,这对于这个相对简单的状态对象来说是不必要的冗长并且容易出错。 我们还必须触及国家的每一个部分,这是不必要的。 让我们看看如何使用 Immer 来简化它。

 const updateReducerWithProduce = (state = initState, action) => produce(state, draft => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'UPDATE_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
并且通过几行代码,我们大大简化了我们的 reducer。 此外,如果我们陷入默认情况,Immer 只会返回草稿状态,而我们不需要做任何事情。 请注意如何减少样板代码和消除状态传播。 使用 Immer,我们只关心我们想要更新的状态部分。 如果我们找不到这样的项目,例如在 `UPDATE_INSTALLED` 操作中,我们只需继续前进,而无需触及任何其他内容。 `produce` 函数也适用于柯里化。 将回调作为第一个参数传递给 `produce` 旨在用于柯里化。 咖喱“produce”的签名是
//curried produce signature produce(callback) => (state) => nextState
让我们看看如何用咖喱产品更新我们之前的状态。 我们的咖喱产品看起来像这样:
 const curriedProduce = produce((draft, action) => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'SET_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });

咖喱产物函数接受一个函数作为其第一个参数并返回一个咖喱​​产物,它现在只需要一个状态来产生下一个状态。 该函数的第一个参数是草稿状态(将派生自调用此咖喱产品时要传递的状态)。 然后跟随我们希望传递给函数的每个参数数量。

为了使用这个函数,我们现在需要做的就是传入我们想要从中产生下一个状态的状态和像这样的动作对象。

 // add a new package to the starting state const nextState = curriedProduce(initState, { type: 'ADD_PACKAGE', package: newPackage, }); // update an item in the recently produced state const nextState2 = curriedProduce(nextState, { type: 'SET_INSTALLED', name: 'immer', installed: true, });

请注意,在 React 应用程序中使用useReducer钩子时,我们不需要像我上面所做的那样显式传递状态,因为它会处理这些。

你可能想知道,Immer 会像现在 React 中的所有东西一样得到一个hook吗? 好吧,你有好消息陪伴。 Immer 有两个处理状态的钩子: useImmeruseImmerReducer钩子。 让我们看看它们是如何工作的。

使用useImmeruseImmerReducer Hooks

useImmer钩子的最佳描述来自 use-immer README 本身。

useImmer(initialState)useState非常相似。 函数返回一个元组,元组的第一个值是当前状态,第二个是updater函数,它接受一个immer producer函数,在这个函数中可以自由地对draft进行变异,直到producer结束并进行更改不可变并成为下一个状态。

要使用这些钩子,除了主要的 Immer 库之外,您还必须单独安装它们。

 yarn add immer use-immer

在代码方面, useImmer钩子如下所示

import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)

就这么简单。 你可以说它是 React 的 useState 但有点类固醇。 使用更新功能非常简单。 它接收草稿状态,您可以像下面那样随意修改它。

 // make changes to data updateData(draft => { // modify the draft as much as you want. })

Immer 的创建者提供了一个代码沙盒示例,您可以尝试看看它是如何工作的。

如果你使用过 React 的useReducer钩子,那么useImmerReducer也同样易于使用。 它有一个相似的签名。 让我们看看用代码术语来说是什么样子的。

 import React from "react"; import { useImmerReducer } from "use-immer"; const initState = {} const reducer = (draft, action) => { switch(action.type) { default: break; } } const [data, dataDispatch] = useImmerReducer(reducer, initState);

我们可以看到 reducer 收到了一个draft状态,我们可以随意修改它。 这里还有一个代码框示例供您试验。

这就是使用 Immer hooks 的简单程度。 但是,如果您仍然想知道为什么要在项目中使用 Immer,这里总结了我发现的使用 Immer 的一些最重要的原因。

为什么你应该使用 Immer

如果您已经编写了任何时间的状态管理逻辑,您将很快体会到 Immer 提供的简单性。 但这并不是 Immer 提供的唯一好处。

当你使用 Immer 时,你最终会编写更少的样板代码,正如我们在相对简单的 reducer 中看到的那样。 这也使得深度更新相对容易。

使用 Immutable.js 等库,您必须学习新的 API 才能获得不变性的好处。 但是使用 Immer,您可以使用普通的 JavaScript ObjectsArraysSetsMaps来实现相同的目标。 没有什么新东西要学。

Immer 还默认提供结构共享。 这仅仅意味着当您对状态对象进行更改时,Immer 会自动在新状态和先前状态之间共享状态中未更改的部分。

使用 Immer,您还可以自动冻结对象,这意味着您无法更改produced的状态。 例如,当我开始使用 Immer 时,我尝试将sort方法应用于 Immer 的生产函数返回的对象数组。 它抛出了一个错误,告诉我无法对数组进行任何更改。 在应用sort之前,我必须应用数组切片方法。 再一次,生成的nextState是不可变的状态树。

Immer 也是强类型的,gzip 压缩后非常小,只有 3KB。

结论

在管理状态更新方面,使用 Immer 对我来说是轻而易举的事。 这是一个非常轻量级的库,可以让您继续使用您所学过的有关 JavaScript 的所有内容,而无需尝试学习全新的东西。 我鼓励您将它安装在您的项目中并立即开始使用它。 您可以在现有项目中添加使用它并逐步更新您的减速器。

我还鼓励您阅读 Michael Weststrate 的 Immer 介绍性博客文章。 我觉得特别有趣的部分是“Immer 是如何工作的?” 这部分解释了 Immer 如何利用代理等语言功能和写时复制等概念。

我还建议您阅读这篇博文:JavaScript 中的不变性:一种对比观点,作者 Steven de Salas 介绍了他对追求不变性的优点的看法。

我希望通过您在这篇文章中学到的东西,您可以立即开始使用 Immer。

相关资源

  1. use-immer , GitHub
  2. 伊默,GitHub
  3. function , MDN 网络文档, Mozilla
  4. proxy ,MDN 网络文档,Mozilla
  5. 对象(计算机科学),维基百科
  6. “JS 中的不变性”,Orji Chidi Matthew,GitHub
  7. “ECMAScript 数据类型和值”,Ecma International
  8. JavaScript 的不可变集合,Immutable.js,GitHub
  9. “不变性的案例”,Immutable.js,GitHub