购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.1 用reducer来管理复杂状态

问题

很多React组件都很简单。它们执行一些简单操作,例如渲染一段HTML,或许也展示一些属性。

然而,有些组件可能复杂得多。它们需要管理多个内部状态。例如,如图3-1所示的是一个简单的数字游戏。

图3-1:一个简单的数字拼图游戏

该组件在网格中展示了一些数字方块和一个空网格。用户可以通过单击空网格旁边的数字方块来对方块进行移动。通过这种方式,用户可以不断重新排列数字方块,直到它们按照从1到8的顺序排列。

该组件渲染的页面元素并不多,但它还需要处理一些相当复杂的逻辑和数据。例如,组件需要记录方块的位置,需要判断用户是否可以移动给定的方块,需要知道如何移动方块,需要判断游戏是否完成。组件还需要做一些其他事情,比如通过重新打乱方块来重置游戏。

我们可以在组件内部实现这些代码的编写,但是测试会比较困难。你可以使用React Testing Library,但它可能有点大材小用了,因为大多数代码与渲染HTML几乎没有关系。

解决方案

如果你的组件有复杂的内部状态,或者需要用复杂的方式来管理状态,那么可以考虑使用reducer。

reducer是一个接受两个参数的函数:

·一个用于表示给定状态的对象或数组

·一个用于描述你将如何修改状态的动作(action)

我们给reducer函数传递状态对象,然后它会返回一个新状态的副本。

action参数可以是你想要的任何参数,但通常action会有一个名为type的字符串属性,以及一个名为payload的带有附加信息的属性。你可以将type视为命令的名称,将payload视为命令的参数。

例如,如果我们将数字方块的位置编号为从0(左上角)到8(右下角),那么我们可以用如下命令告诉reducer移动位于左上角的方块:

我们需要一个定义游戏所有内部状态的对象或数组。此处使用一个简单的字符串数组:

它将表示数字方块的布局如下:

然而,更灵活的方法是用一个对象来管理状态,这个对象有一个包含当前布局的items属性:

为什么要使用对象来管理状态呢?因为它将允许reducer返回更多状态值,比如游戏是否完成:

在确定了move的action,和返回状态的数据结构后,下面我们可以着手创建测试代码了:

在第一个测试用例中,我们在一个状态中记录方块的位置信息。然后我们检查reducer是否在新的状态中返回新的位置信息。

在第二个测试用例中,我们移动两个方块,然后通过complete属性来告诉我们游戏已经结束。

现在,reducer代码已经相当长了:

我们的reducer目前只识别一个action:move。GitHub仓库上本例的完整代码( https://oreil.ly/q85H3 )还包括shuffle和reset这两个action。仓库还提供了一组更详尽的测试( https://oreil.ly/yRNyU ),我们写之前的代码时用到过。

但是,这些代码都不包括任何React组件。它是纯粹的JavaScript代码,因此可以被完全独立地创建和测试。

在reducer中生成一个新对象来表示新状态时,要小心确保每个新状态完全独立于之前的状态。

现在是时候用useReducer Hook将我们的reducer连接到React组件中了:

尽管我们的拼图组件执行的操作相当复杂,但实际的React代码却相对较短。

useReducer接受一个reducer函数和一个初始状态,并返回一个具有两个元素的数组:

·数组中的第一个元素是reducer的当前状态;

·第二个元素是一个dispatch函数,它允许我们向reducer发送action(动作)。

我们通过遍历state.items数组中的字符串来显示数字方块。

如果用户单击位置i的方块,我们就向reducer发送一个move命令:

React组件并不知道如何移动方块,甚至不知道自己是否能移动方块。React组件会将action发送给reducer。

如果move动作移动了一个方块,组件将根据新的位置信息重新渲染组件。如果游戏完成了,组件将通过state.complete的值来获取到是否完成的标志位。

我们还添加了两个按钮来运行shuffle和reset动作(在前边的代码中省略了),你可以在GitHub仓库( https://oreil.ly/WmZ18 )中找到它们。

现在已经完成了组件的功能,让我们来试一试。当我们第一次加载组件时,它处于初始状态,如图3-2所示。

图3-2:游戏的初始状态

如果我们单击标签为7的方块,它就会移动到空白格中(如图3-3所示)。

图3-3:移动方块7以后

单击SHUFFLE按钮,reducer会随机重新排列方块,如图3-4所示。

图3-4:SHUFFLE按钮将方块移动到随机位置

如果我们单击RESET,拼图就会变成完成状态的位置,弹出“Complete!”文本(如图3-5所示)。

图3-5:RESET按钮将方块移动到完成状态的位置

我们将所有的复杂性都隐藏在reducer函数中了,我们可以单独对其进行测试,而且React组件的易用性和可维护性也很好。

讨论

reducer是管理复杂性的一种方法。你可以考虑在以下两种情况下使用它:

·你有大量的内部状态需要管理。

·你需要复杂的逻辑来管理组件的内部状态。

如果其中任何一种情况成立,那么reducer可以很大程度地降低代码管理的难度。

但是,对于非常小的组件来说,使用reducer时要小心。如果你的组件的状态很简单,逻辑很少,使用reducer反而会增加复杂性。

有时候,即使你的状态很复杂,也可以采用reducer之外的其他方法。例如,如果要捕获和验证表单中的数据,最好创建一个验证表单组件(请参阅本书3.3节)。

你需要确保reducer没有任何副作用。例如,避免进行会更新服务器的网络调用。如果你的reducer有副作用,那么它很可能会崩溃。React有时可能会在开发模式下偷偷地调用你的reducer,以确保没有副作用产生。如果你正在使用reducer并注意到React在渲染组件时两次调用你的代码,这意味着React正在检查异常行为。

在这些条件下,reducer是降低系统复杂度十分有效的工具。reducer与Redux等库紧密结合,可以方便地进行复用,简化了组件的结构,并且使得React代码更易于测试。

你可以从GitHub网站( https://oreil.ly/q85H3 )下载本解决方案的源代码。 4oQ6pi3VTwtbsWxfhd7m6FeAV1MEjFXeoF2R/FKUm+0rVAbe6gO9Kep2BDS5vCoj

点击中间区域
呼出菜单
上一章
目录
下一章
×