Open ychow opened 5 years ago
很棒的解读 支持下
还是不太理解 useState 和 useReducer
// useReducer
const [state, setState] = useReducer((state, newState) => (
{ ...state, ...newState }
));
// useState
const [state, setState] = useState({})
setState({...state, ...newState})
以上的结果其实是一样的,如果不用 redux 的范式,那为什么不统一用 useState 呢?
@think2011 其实useState应该是useReducer的简版。 如果state较为复杂,那么useReducer的优势是非常明显的。
很多人对React新增的Hooks非常感兴趣,其中也包括我。
当看完了如何使用Hooks的教程后,接着你就会考虑:如何使用Hooks来重构你的应用或React组件?中途又会遇到哪些问题?
简介
这篇文章所要介绍的内容是比较简单的,但是和其他文章的思路不同。关于Hooks,大部分作者习惯于使用一个现成的应用,一步步演示如何使用Hooks来重构。但是这还不足够好。
为什么?因为每一个应用都有其特定的使用场景。
这篇文章我会演示众多应用中普遍存在的一些问题。当然了,我们会通过几个范例,逐步由浅入深。
为什么使用React Hooks重构
我不打算解释你为什么要这样做。如果你想知道其理由,可以去官方文档中查看。
开始之前
这篇文章需要你具备React Hooks 的基本使用。如果你想再学习下,这篇文章也许可以帮助你。
接下来我们就可以开始看看当你使用Hooks来重构你的应用所会遇到的问题。
第一个问题: 如何把类组件转成函数式组件(function component)
这个问题是在当你开始使用React Hooks来重构你的应用时,你所面临的第一个。
这个问题很简单:在保证功能的前提下,如何把类组件转成函数式组件?
我们通过一些范例来演示,让我们从最简单的开始。
1. 没有状态(state)或生命周期函数的类组件
上面的GIF图片对于一些高级的开发者已经足够了解把类组件重构成函数式组件的区别。但是为了可读性,以及其他开发者,我会把解释以及相关的代码展示出来。
下面是一个非常基础的例子:一个只会渲染
JSX
代码的类组件。重构这样的组件是非常快的。请看:
两者的不同之处?
class
关键字;使用了函数来代替this
; 而是使用了函数作用域来调用上面没有什么特别重要的难点,让我们继续吧。
2. 带有prop类型声明,并且赋有默认值的类组件
这是另一个简单的例子。思考下面的代码:
上面的代码重构后:
重构成函数式组件后,看起来更简单了。props变成了函数参数,其默认值通过ES6的参数默认值来解决,并且
static propTypes
被替换成App.propTypes
。继续。3. 带有状态(state)的类组件(单个或多个值)
当你的组件里含有真实的状态时,这时就开始变得有趣多了。可能你的大部分组件都符合这种情况或者比它更复杂一些。
思考下面的代码:
这个组件的状态里只有一个属性。足够简单了!
我们可以使用
useState
来重构,请看代码:看起来更简单了!
重构后也能正常使用
如果组件状态内部有多个属性,你可以调用多个
useState
,那还是可以接受的,比如下面的代码:这些都是比较基础的,如果你想看更多的例子,可以点击这里。
权衡Hooks的使用
虽然使用Hooks来重构你的应用或组件非常有趣,但它需要耗费一定的时间和人力来保持更新。
如果你碰巧在维护比较庞大的代码库,那么你在开始采用Hooks之前就需要权衡其代价。比如下面的场景。
思考下面的代码:
当这个组件
mounted
时,会发起一个服务器请求去获取相应的数据,然后根据返回的结果设置state
。在这里我们就不讨论里面的异步逻辑是如何运行的,而是关注下
setState
。这里给
setState
传了一个带有4个属性的对象,但是在实际应用里的setState
方法我们会传递更多的属性。如果使用React Hooks来做,你极有可能会使用多个
useState
来拆分里面的属性。你也可以直接传一个对象给useState
,但是由于这些属性都是没有关联的,而且也给以后要把它们拆分成独立的useState
增加了难度。所以重构后的样子可能是这样:
等等 - 这还不是最终所期望的!
当然
this.setState
方法也会变成下面这样:虽然这样它是可以正常运行的。但是,如果你的组件里面有更多的
setState
方法,无论你是直接重写它们或是把它们放在另一个自定义的Hooks里,你都需要花费更多的时间。如果你想使用Hooks来提升你的代码,而且代码改动更小,还能拥有和
setState
差不多的功能?可以实现吗?在接下来的例子中,你需要做一些取舍。下面,我们将会介绍
useReducer
钩子。useReducer
的用法如下:reducer
是一个接收state
和action
,并且返回一个newState
的函数。对组件内部的
state
处理后,reducer就会返回newState
。如果你之前使用过
redux
,那你就知道action
必须接收一个带有type
属性的对象。然而,在useReducer
中,reducer
函数可以接收一个state
以及多个action
,然后返回一个新的state对象。我们可以采用这个特点,可以让我们重构不再那么痛苦。就像下面这样:
上面的代码有什么效果?
你可以看到,我们没有在组件内部直接去修改
this.setState
的用法,而是选择了一个更简单,避免更多代码改动的方法。如果使用Hooks, 我们可以把
this.setState({data, error: null, loaded: null, fetching: false})
里的this.
去掉,而且它依然可以正常运行。下面的代码就是还能正常运行的原因:
当你尝试更新state的时候,你传入
setState
(和redux里的dispatch
类似)的值将会被当作reducer的第二个参数,也就是newState
。在
Redux
中,你需要书写switch
,而如果使用setState
,我们只需要传入新的状态对象,它就会覆盖掉旧数据。也就是更新状态里的属性值,而不是整个替换掉。有了这种方案,我们就可以有更少的代码改动,还可以有一个相同功能的
setState
,这样可以让我们使用Hooks来改善代码变得更容易。下面是最少代码改动后完整的代码:
简化版的生命周期函数
在重构时,你将会面临到的的另一个挑战就是重构
componentDidMount
,componentWillUnMount
以及componentDidUpdate
这几个生命周期函数里的逻辑。正确的做法是,把这些逻辑(有副作用的函数)放到
useEffect
中。但是需要注意的是,每次render
时,useEffect
里的代码都会运行一次。如果你对Hooks
比较熟悉了,你应该知道这一点。那还有哪些特性呢?
useEffect
还有一个有趣的特性就是,他还有第二个参数,接收一个数组。思考下面传了一个空数组的例子:
传入一个空数组,可以让
useEffect
里的副作用代码只在组件挂载以及卸载的时候运行。这对于你只想在组件挂载的时候,去请求数据是非常好的实现方式。下面是传了非空数组的例子:
这个例子就会在组件挂载时运行一次
useEffect
里的代码,以及当变量name
的值发生改变时,它也会执行一次。useEffect中对象的比较
useEffect
中可以通过传一个函数进去执行有副作用的代码。当然,它也接收第二个参数: 一个可以决定副作用函数执行的数组。比如:
上面的代码中,只有当
name
的值变化时,doSomething
函数才会被执行。默认doSomething
函数会在每次render
的时候执行一次, 如果你不想这样,那么上面这种用法对你就有帮助。然而,这会引起另一个担忧。为了实现
useEffects
中只有当name
变量更改的时候才执行doSomething
函数,它就需要去比较现在的值是否不同于旧数据,比如,prevName === name
。对于javascript基础数据类型它是没问题的。
但是如果
name
是一个对象呢?Javascript中的对象是通过指针比较!从技术上说,如果name
是一个对象,那么每次render的时候它总是不同的,所以每次比较的时候prevName === name
总会得到false
。从应用角度考虑,每次render的时候,
domeSomething
都会执行一次,势必会造成应用的性能问题。那么有方法可以解决吗?思考下面的简单组件:
这个组件渲染了一个button 和一个随机数。点击按钮,一个新的随机数就会被生成。
注意
useEffects
钩子的运行是由变量name
来决定的。在这个例子中,
name
是一个简单的字符串。当组件进入挂载阶段时,副作用代码就会被执行,因此,console.log("Effect has been run!"
也就会被调用。render(渲染)过后,一个浅比较就会被执行,比如,
prevName === name
,其中prevName
表示渲染之前的值。字符串比较只是比较它们的值,因此
"name" === "name"
总会是 true。 因此副作用代码就不会执行。于是,你可以看到只输出了一次
Effect has been run!
.现在,把
name
改成一个对象。在这个例子中,第一次render过后,浅比较就会被再次执行。然而,由于对象比较是比较其指针,而不是值,所以比较会失败。比如,下面的表达式语句会返回
false
:因此,每次渲染后副作用代码都会执行一次,你也就会看到有许多logs打印。
那我们如何才能阻止这种情况发生呢?
第一个方案: 使用JSON.stringify
该解决方案如下:
通过使用
JSON.stringify(name)
,把它们转换成字符串,现在他们的比较就是比较他们的值。这个可行,但是需要谨慎使用。只有当对象不是特别复杂,层级不是特别深的时候,才可以使用。
第二个方案:使用自定义判断
这个办法涉及到追踪之前的值 — 在这个例子中需要追踪
name
, 然后需要对其当前的值进行一个深比较。代码可能有点多,但是可以达到想要的效果:
现在,在运行副作用代码前,我们会比较它们的值是否相等:
但是,
prevName.current
是啥?在Hooks中,你可以使用useRef
钩子来追踪它们的值。在上面的例子中,关键代码如下:这里记录了最开始
useEffect
钩子里使用的name
的值。我知道这个理解起来有难度,所以我在下面展示一个标注了所有注释的代码:第三个方案:使用useMemo钩子
我觉得这个方案是比较优雅的。代码如下:
useEffect
钩子仍然传了name变量,但是name
的值,现在是由useMemo
来生成。useMemo
接收一个函数,然后返回相应的值。在这个例子中,返回了{firstName: "name"}
。useMemo
的第二个参数和useEffects
类似,都是接收一个数组,该数组里的值也决定其是否运行。如果没有传数组,那该值的计算将在每次render的时候重新计算。传一个空数组,只会在组件挂载阶段计算它的值,而不会在所有render状态期间重复计算。这就让
name
在所有render期间都保持着同一个值(指针)。正如上面解释的,虽然现在
name
是一个对象,但是它还能如我们期望的正常运行,而且没有多次运行副作用代码。name
现在是一个在所有render期间都拥有相同指针的memoized的对象。useEffect导致测试失败?
在使用Hooks重构你的应用或是组件时,困扰之一就是以前写的测试会跑失败 — 但是找不到失败的原因。
当你遇到这种情况的时候,你会感觉很沮丧,因为你知道肯定是有原因会导致测试失败的。
在使用
useEffect
时,你需要特别注意的是,它的回调函数不是同步的,而是在render之后才运行的。所以,useEffect
并不是componentDidMount
+componentDidUpdate
+componentWillUnmount
。由于"async"的行为,当你采用
useEffect
时,原来老的测试一部分(如果不是全部)可能会失败。有什么解决方法吗?
在这些例子中,使用 react-test-utils 里的
act
对我们很有帮助。如果在你的测试中使用 react-testing-library,act
有着很重要的作用。在使用react-testing-library
时,你还需要使用act
手动来包裹,比如状态更新或是触发事件。关于这个的讨论,可以查看这里 。想在
act
里执行异步操作?你也可以看看这里的讨论 。你可能认为关于使用
act
的解决方法我会轻轻带过。其实我是准备把里面的每个细节都罗列出来,但是Suni Pai 已经说过了。如果你认为官方文档没有很好的解释 — 我也同意这个观点 — 你可以从这个仓库里看到更多实践act
的例子。另一个和测试失败有关的问题就是当你使用类似Enzyme这样的测试库的时候,并且在你的测试中有许多的实现细节,比如,比如调用
instance()
和state()
。在这些例子中,你的测试会跑失败,仅仅只是因为你把组件重构成了函数式组件。更安全的方式来重构你的render props API
我不知道你们的情况,我会在任何地方使用render props API 。基于Hooks来重构一个使用了render props API的组件其实也不是特别困难。虽然还是有一个小问题。
思考下面这个返回了一个props API 的组件:
这是一个特别设计的例子,但是已经足够了!下面是它的用法:
渲染
ConsumeTrivialRenderProps
组件, 并且渲染从render props API传过来的loading
和data
的值。目前为止,一切都还好!
render props的问题在于它会让你的代码比你想象得更加嵌套。正如前面所说,使用Hooks来重构
TrivialRenderProps
组件不是特别困难的事儿。为了重构它,你只需要实现一个自定义的Hooks,来包裹组件,然后返回之前一样的数据就行。重构完后,就是下面这样:
看起来非常简洁!
下面是自定义的钩子函数
useTrivialRenderProps
:完成!
那么这里有什么问题吗?
当你在开发大型的代码库时,你可能会在许多不同的地方使用render props API。把组件的实现方式更改为Hooks,意味着你需要去更改许多不同地方的代码。
那我们可以做一些取舍吗?当然了!
你可以使用Hooks去重构组件,同时也可以返回一个render props API。这样的话,你就可以使用Hooks来重构了,而不至于过多的修改代码。
例如:
现在,通过导出两种实现方式,你可以在你整个代码库里去采用Hooks了。因为不管是之前的实现方式,还是使用Hooks的方式,现在都可以正常工作。
处理状态初始值
在类组件中,一种常见的情景就是,某些状态的初始值是根据某些计算得到的。比如:
这是一个可以展示一类问题的简单例子。在组件挂载阶段,是有可能出现,在
constructor
里经过计算才得到初始值的情况。在这个例子中,我们判断props是否有传过来token,如果有,就赋值给token;或者是判断本地存储里是否有
app-token
,如果有并且有值的话,同样的赋值给token。在使用Hooks重构时,如何来处理设置初始值的逻辑?一个鲜为人知的特性就是,
useState
钩子可以接受一个initialState
的参数,这个参数也可以是一个函数。不管这个函数返回了什么,它都会被当作
initialState
。下面就是经过Hooks重构的组件代码:从技术上来说,整个的逻辑是一样的。最主要的地方在于如果你想通过一些逻辑来初始化状态,你可以传一个函数给
useState
。最后
使用Hooks来重构你的应用并不是必须得做的。需要通过你自己以及你所在的团队来商量权衡。如果你决定使用Hooks API来重构你的应用,那么我希望这篇文章能给你一些好的建议。