[译]React Hooks 实践:如何使用Hooks重构你的应用
原文链接: https://blog.logrocket.com/practical-react-hooks-how-to-refactor-your-app-to-use-hooks-b1867e7b0a53
很多人对React新增的Hooks非常感兴趣,其中也包括我。
当看完了如何使用Hooks的教程后,接着你就会考虑:如何使用Hooks来重构你的应用或React组件?中途又会遇到哪些问题?
简介
这篇文章所要介绍的内容是比较简单的,但是和其他文章的思路不同。关于Hooks,大部分作者习惯于使用一个现成的应用,一步步演示如何使用Hooks来重构。但是这还不足够好。
为什么?因为每一个应用都有其特定的使用场景。
这篇文章我会演示众多应用中普遍存在的一些问题。当然了,我们会通过几个范例,逐步由浅入深。
为什么使用React Hooks重构
我不打算解释你为什么要这样做。如果你想知道其理由,可以去官方文档中查看。
开始之前
这篇文章需要你具备React Hooks 的基本使用。如果你想再学习下,这篇文章也许可以帮助你。
接下来我们就可以开始看看当你使用Hooks来重构你的应用所会遇到的问题。
第一个问题: 如何把类组件转成函数式组件(function component)
这个问题是在当你开始使用React Hooks来重构你的应用时,你所面临的第一个。
这个问题很简单:在保证功能的前提下,如何把类组件转成函数式组件?
我们通过一些范例来演示,让我们从最简单的开始。
1. 没有状态(state)或生命周期函数的类组件

上面的GIF图片对于一些高级的开发者已经足够了解把类组件重构成函数式组件的区别。但是为了可读性,以及其他开发者,我会把解释以及相关的代码展示出来。
下面是一个非常基础的例子:一个只会渲染 JSX 代码的类组件。
// before
import React, {Component} from 'react';
class App extends Component {
handleClick = () => {
console.log("helloooooo")
}
render() {
return <div>
Hello World
<button onClick={this.handleClick}>
Click me!
</button>
</div>
}
}
export default App
重构这样的组件是非常快的。请看:
// after
import React from 'react'
function App() {
const handleClick = () => {
console.log("helloooooo")
}
return <div>
Hello World
<button onClick={handleClick}> Click me! </button>
</div>
}
export default App
两者的不同之处?
- 没有了
class关键字;使用了函数来代替 - 在函数式组件里没有
this; 而是使用了函数作用域来调用
上面没有什么特别重要的难点,让我们继续吧。
2. 带有prop类型声明,并且赋有默认值的类组件

这是另一个简单的例子。思考下面的代码:
// before
class App extends Component {
static propTypes = {
name: PropTypes.string
}
static defaultProps = {
name: "Hooks"
}
handleClick = () => {
console.log("helloooooo")
}
render() {
return <div>
Hello {this.props.name}
<button onClick={this.handleClick}> Click me! </button>
</div>
}
}
上面的代码重构后:
function App({name = "Hooks"}) {
const handleClick = () => {
console.log("helloooooo")
}
return <div>
Hello {name}
<button onClick={handleClick}>Click me! </button>
</div>
}
App.propTypes = {
name: PropTypes.number
}
重构成函数式组件后,看起来更简单了。props变成了函数参数,其默认值通过ES6的参数默认值来解决,并且static propTypes 被替换成 App.propTypes 。继续。
3. 带有状态(state)的类组件(单个或多个值)
当你的组件里含有真实的状态时,这时就开始变得有趣多了。可能你的大部分组件都符合这种情况或者比它更复杂一些。
思考下面的代码:
class App extends Component {
state = {
age: 19
}
handleClick = () => {
this.setState((prevState) => ({age: prevState.age + 1}))
}
render() {
return <div>
Today I am {this.state.age} Years of Age
<div>
<button onClick={this.handleClick}>Get older! </button>
</div>
</div>
}
}
这个组件的状态里只有一个属性。足够简单了!

我们可以使用useState 来重构,请看代码:
function App() {
const [age, setAge] = useState(19);
const handleClick = () => setAge(age + 1)
return <div>
Today I am {age} Years of Age
<div>
<button onClick={handleClick}>Get older! </button>
</div>
</div>
}
看起来更简单了!

重构后也能正常使用
如果组件状态内部有多个属性,你可以调用多个useState ,那还是可以接受的,比如下面的代码:
function App() {
const [age, setAge] = useState(19);
const [status, setStatus] = useState('married')
const [siblings, setSiblings] = useState(10)
const handleClick = () => setAge(age + 1)
return <div>
Today I am {age} Years of Age
<div>
<button onClick={handleClick}>Get older! </button>
</div>
</div>
}
这些都是比较基础的,如果你想看更多的例子,可以点击这里。
权衡Hooks的使用
虽然使用Hooks来重构你的应用或组件非常有趣,但它需要耗费一定的时间和人力来保持更新。
如果你碰巧在维护比较庞大的代码库,那么你在开始采用Hooks之前就需要权衡其代价。比如下面的场景。
思考下面的代码:
const API_URL = "https://api.myjson.com/bins/19enqe";
class App extends Component {
state = {
data: null,
error: null,
loaded: false,
fetching: false,
}
async componentDidMount() {
const response = await fetch(API_URL)
const { data, status } = {
data: await response.json(),
status: response.status
}
// error?
if (status !== 200) {
return this.setState({
data,
error: true,
loaded: true,
fetching: false,
})
}
// no error
this.setState({
data,
error: null,
loaded: true,
fetching: false,
})
}
render() {
const { error, data } = this.state;
return error ? <div> Sorry, and error occured :( </div> :
<pre>{JSON.stringify(data, null, ' ')}</pre>
}
}
当这个组件mounted 时,会发起一个服务器请求去获取相应的数据,然后根据返回的结果设置 state 。
在这里我们就不讨论里面的异步逻辑是如何运行的,而是关注下 setState 。
class App extends Component {
...
async componentDidMount() {
...
if (status !== 200) {
return this.setState({
data,
error: true,
loaded: true,
fetching: false,
})
}
this.setState({
data,
error: null,
loaded: true,
fetching: false,
})
}
render() {
...
}
}
这里给setState传了一个带有4个属性的对象,但是在实际应用里的setState方法我们会传递更多的属性。
如果使用React Hooks来做,你极有可能会使用多个useState来拆分里面的属性。你也可以直接传一个对象给 useState ,但是由于这些属性都是没有关联的,而且也给以后要把它们拆分成独立的 useState 增加了难度。
所以重构后的样子可能是这样:
...
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loaded, setLoading] = useState(false);
const [fetching, setFetching] = useState(false);
...
等等 - 这还不是最终所期望的!
当然 this.setState 方法也会变成下面这样:
// no more this.setState calls - use updater functions.
setData(data);
setError(null);
setLoading(true);
fetching(false);
虽然这样它是可以正常运行的。但是,如果你的组件里面有更多的 setState 方法,无论你是直接重写它们或是把它们放在另一个自定义的Hooks里,你都需要花费更多的时间。
如果你想使用Hooks来提升你的代码,而且代码改动更小,还能拥有和 setState 差不多的功能?可以实现吗?
在接下来的例子中,你需要做一些取舍。下面,我们将会介绍 useReducer 钩子。
useReducer 的用法如下:
const [state, dispatch] = useReducer(reducer)
reducer 是一个接收 state 和 action ,并且返回一个 newState 的函数。
const [state, dispatch] = useReducer((state, action) => newState)
对组件内部的 state 处理后,reducer就会返回newState 。
如果你之前使用过redux ,那你就知道action 必须接收一个带有type属性的对象。然而,在useReducer 中,reducer 函数可以接收一个 state 以及多个 action ,然后返回一个新的state对象。
我们可以采用这个特点,可以让我们重构不再那么痛苦。就像下面这样:
...
function AppHooks() {
...
const [state, setState] = useReducer((state, newState) => (
{...state, ...newState}
));
setState({
data,
error: null,
loaded: true,
fetching: false,
})
}
上面的代码有什么效果?
你可以看到,我们没有在组件内部直接去修改 this.setState 的用法,而是选择了一个更简单,避免更多代码改动的方法。
如果使用Hooks, 我们可以把this.setState({data, error: null, loaded: null, fetching: false}) 里的 this. 去掉,而且它依然可以正常运行。
下面的代码就是还能正常运行的原因:
const [state, setState] = useReducer((state, newState) => (
{ ...state, ...newState }
));
当你尝试更新state的时候,你传入setState(和redux里的dispatch类似)的值将会被当作reducer的第二个参数,也就是 newState。
在 Redux 中,你需要书写switch,而如果使用 setState ,我们只需要传入新的状态对象,它就会覆盖掉旧数据。也就是更新状态里的属性值,而不是整个替换掉。
有了这种方案,我们就可以有更少的代码改动,还可以有一个相同功能的setState,这样可以让我们使用Hooks来改善代码变得更容易。
下面是最少代码改动后完整的代码:

function AppHooks() {
const initialState = {
data: null,
error: null,
loaded: false,
fetching: false,
}
const reducer = (state, newState) => ({ ...state, ...newState })
const [state, setState] = useReducer(reducer, initialState);
async function fetchData() {
const response = await fetch(API_URL);
const { data, status } = {
data: await response.json(),
status: response.status
}
// error?
if (status !== 200) {
return setState({
data,
error: true,
loaded: true,
fetching: false,
})
}
// no error
setState({
data,
error: null,
loaded: true,
fetching: false,
})
}
useEffect(() => {
fetchData()
}, [])
const { error, data } = state
return error ? <div> Sorry, and error occured :( </div> :
<pre>{JSON.stringify(data, null, ' ')}</pre>
}
简化版的生命周期函数
在重构时,你将会面临到的的另一个挑战就是重构componentDidMount, componentWillUnMount 以及componentDidUpdate 这几个生命周期函数里的逻辑。
正确的做法是,把这些逻辑(有副作用的函数)放到useEffect 中。但是需要注意的是,每次render 时,useEffect 里的代码都会运行一次。如果你对Hooks比较熟悉了,你应该知道这一点。
import { useEffect } from 'react'
useEffect(() => {
// your logic goes here
// optional: return a function for canceling subscriptions
return () = {}
})
那还有哪些特性呢?
useEffect 还有一个有趣的特性就是,他还有第二个参数,接收一个数组。
思考下面传了一个空数组的例子:
import { useEffect } from 'react'
useEffect(() => {
}, []) // 👈 array argument
传入一个空数组,可以让useEffect里的副作用代码只在组件挂载以及卸载的时候运行。这对于你只想在组件挂载的时候,去请求数据是非常好的实现方式。
下面是传了非空数组的例子:
import { useEffect } from 'react'
useEffect(() => {
}, [name]) // 👈 array argument with a value
这个例子就会在组件挂载时运行一次useEffect 里的代码,以及当变量name的值发生改变时,它也会执行一次。
useEffect中对象的比较
useEffect中可以通过传一个函数进去执行有副作用的代码。
useEffects(doSomething)
当然,它也接收第二个参数: 一个可以决定副作用函数执行的数组。比如:
useEffects(doSomething, [name])
上面的代码中,只有当name 的值变化时,doSomething函数才会被执行。默认doSomething函数会在每次 render 的时候执行一次, 如果你不想这样,那么上面这种用法对你就有帮助。
然而,这会引起另一个担忧。为了实现useEffects 中只有当name变量更改的时候才执行doSomething函数,它就需要去比较现在的值是否不同于旧数据,比如,prevName === name。
对于javascript基础数据类型它是没问题的。
但是如果name是一个对象呢?Javascript中的对象是通过指针比较!从技术上说,如果 name是一个对象,那么每次render的时候它总是不同的,所以每次比较的时候prevName === name 总会得到 false。
从应用角度考虑,每次render的时候,domeSomething都会执行一次,势必会造成应用的性能问题。那么有方法可以解决吗?
思考下面的简单组件:
function RandomNumberGenerator() {
const name = "name"
useEffect(() => {
console.log("Effect has been run!")
}, [name])
const [randomNumber, setRandomNumber] = useState(0);
return <div>
<h1>{randomNumber}</h1>
<button onClick={() => {setRandomNumber(Math.random() }}>
Generate random number!
</button>
</div>
}
这个组件渲染了一个button 和一个随机数。点击按钮,一个新的随机数就会被生成。

注意useEffects钩子的运行是由变量name来决定的。
在这个例子中,name是一个简单的字符串。当组件进入挂载阶段时,副作用代码就会被执行,因此,console.log("Effect has been run!" 也就会被调用。
render(渲染)过后,一个浅比较就会被执行,比如,prevName === name,其中prevName表示渲染之前的值。
字符串比较只是比较它们的值,因此"name" === "name"总会是 true。 因此副作用代码就不会执行。
于是,你可以看到只输出了一次Effect has been run!.
现在,把name改成一个对象。
function RandomNumberGenerator() {
// look here 👇
const name = {firstName: "name"}
useEffect(() => {
console.log("Effect has been run!")
}, [name])
const [randomNumber, setRandomNumber] = useState(0);
return <div>
<h1> {randomNumber} </h1>
<button onClick={() => { setRandomNumber(Math.random()) }}>Generate random number!</button>
</div>
}
在这个例子中,第一次render过后,浅比较就会被再次执行。然而,由于对象比较是比较其指针,而不是值,所以比较会失败。比如,下面的表达式语句会返回false:
{firstName: "name"} === {firstName: "name"}
因此,每次渲染后副作用代码都会执行一次,你也就会看到有许多logs打印。

那我们如何才能阻止这种情况发生呢?
第一个方案: 使用JSON.stringify
该解决方案如下:
...
useEffect(() => {
console.log("Effect has been run!")
}, [JSON.stringify(name)])
通过使用JSON.stringify(name),把它们转换成字符串,现在他们的比较就是比较他们的值。
这个可行,但是需要谨慎使用。只有当对象不是特别复杂,层级不是特别深的时候,才可以使用。
第二个方案:使用自定义判断
这个办法涉及到追踪之前的值 — 在这个例子中需要追踪name, 然后需要对其当前的值进行一个深比较。
代码可能有点多,但是可以达到想要的效果:
// the isEqual function can come from anywhere
// - as long as you perform a deep check.
// This example uses a utility function from Lodash
import {isEqual} from 'lodash'
function RandomNumberGenerator() {
const name = {firstName: "name"}
useEffect(() => {
if(!isEqual(prevName.current, name)) {
console.log("Effect has been run!")
}
})
const prevName = useRef;
useEffect(() => {
prevName.current = name
})
const [randomNumber, setRandomNumber] = useState(0);
return <div>
<h1> {randomNumber} </h1>
<button onClick={() => { setRandomNumber(Math.random()) }}>
Generate random number!
</button>
</div>
}
现在,在运行副作用代码前,我们会比较它们的值是否相等:
!isEqual(prevName.current, name)
但是, prevName.current 是啥?在Hooks中,你可以使用useRef钩子来追踪它们的值。在上面的例子中,关键代码如下:
const prevName = useRef;
useEffect(() => {
prevName.current = name
})
这里记录了最开始useEffect 钩子里使用的name的值。我知道这个理解起来有难度,所以我在下面展示一个标注了所有注释的代码:
/**
* 为了正确阅读注释,请先阅读完标注有小乌龟的注释 🐢
// - 从上往下.
* 然后再返回来阅读带有 🦄 的注释 - 从上往下看.
*/
function RandomNumberGenerator() {
// 🐢 1. 在组件最开始的挂载阶段,给name变量赋值
const name = {firstName: "name"}
// 🐢 2. This hook is NOT run. useEffect only runs sometime after render
// 🦄 6. After Render this hook is now run.
useEffect(() => {
// 🦄 7. When the comparison happens, the hoisted value
// of prevName.current is "undefined".
// Hence, "isEqual(prevName.current, name)" returns "false"
// as {firstName: "name"} is NOT equal to undefined.
if(!isEqual(prevName.current, name)) {
// 🦄 8. "Effect has been run!" is logged to the console.
//console.log("Effect has been run!")
}
})
// 🐢 3. The prevName constant is created to hold some ref.
const prevName = useRef;
// 🐢 4. This hook is NOT run
// 🦄 9. The order of your hooks matter! After the first useEffect is run,
// this will be invoked too.
useEffect(() => {
// 🦄 10. Now "prevName.current" will be set to "name".
prevName.current = name;
// 🦄 11. In subsequent renders, the prevName.current will now hold the same
// object value - {firstName: "name"} which is alsways equal to the current
// value in the first useEffect hook. So, nothing is logged to the console.
// 🦄 12. The reason this effect holds the "previous" value is because
// it'll always be run later than the first hook.
})
const [randomNumber, setRandomNumber] = useState(0)
// 🐢 5. Render is RUN now - note that here, name is equal to the object,
// {firstName: "name"} while the ref prevName.current holds no value.
return <div>
<h1>{randomNumber}</h1>
<button onClick={() => { setRandomNumber(Math.random()) }}>
Generate random number!
</button>
</div>
}
第三个方案:使用useMemo钩子
我觉得这个方案是比较优雅的。代码如下:
function RandomNumberGenerator() {
// look here 👇
const name = useMemo(() => ({
firstName: "name"
}), [])
useEffect(() => {
console.log("Effect has been run!")
}, [name])
const [randomNumber, setRandomNumber] = useState(0)
return <div>
<h1>{randomNumber}</h1>
<button onClick={() => { setRandomNumber(Math.random()) }}>
Generate random number!</button>
</div>
}
useEffect钩子仍然传了name变量,但是name的值,现在是由useMemo 来生成。
const name = useMemo(() => ({
firstName: "name"
}), [])
useMemo接收一个函数,然后返回相应的值。在这个例子中,返回了{firstName: "name"}。
useMemo的第二个参数和useEffects类似,都是接收一个数组,该数组里的值也决定其是否运行。如果没有传数组,那该值的计算将在每次render的时候重新计算。
传一个空数组,只会在组件挂载阶段计算它的值,而不会在所有render状态期间重复计算。这就让name在所有render期间都保持着同一个值(指针)。
正如上面解释的,虽然现在name是一个对象,但是它还能如我们期望的正常运行,而且没有多次运行副作用代码。
name 现在是一个在所有render期间都拥有相同指针的memoized的对象。
...
useEffect(() => {
console.log("Effect has been run!")
}, [name]) // 👈 name is memoized!
useEffect导致测试失败?
在使用Hooks重构你的应用或是组件时,困扰之一就是以前写的测试会跑失败 — 但是找不到失败的原因。
当你遇到这种情况的时候,你会感觉很沮丧,因为你知道肯定是有原因会导致测试失败的。
在使用useEffect时,你需要特别注意的是,它的回调函数不是同步的,而是在render之后才运行的。所以,useEffect并不是componentDidMount + componentDidUpdate + componentWillUnmount。
由于"async"的行为,当你采用useEffect时,原来老的测试一部分(如果不是全部)可能会失败。

有什么解决方法吗?
在这些例子中,使用 react-test-utils 里的act 对我们很有帮助。如果在你的测试中使用 react-testing-library,act有着很重要的作用。在使用react-testing-library 时,你还需要使用act 手动来包裹,比如状态更新或是触发事件。
act(() => {
/* fire events that update state */
});
/* assert on the output */
关于这个的讨论,可以查看这里 。想在act 里执行异步操作?你也可以看看这里的讨论 。
你可能认为关于使用act的解决方法我会轻轻带过。其实我是准备把里面的每个细节都罗列出来,但是Suni Pai 已经说过了。如果你认为官方文档没有很好的解释 — 我也同意这个观点 — 你可以从这个仓库里看到更多实践act的例子。
另一个和测试失败有关的问题就是当你使用类似Enzyme这样的测试库的时候,并且在你的测试中有许多的实现细节,比如,比如调用instance() 和 state() 。在这些例子中,你的测试会跑失败,仅仅只是因为你把组件重构成了函数式组件。
更安全的方式来重构你的render props API
我不知道你们的情况,我会在任何地方使用render props API 。基于Hooks来重构一个使用了render props API的组件其实也不是特别困难。虽然还是有一个小问题。
思考下面这个返回了一个props API 的组件:
class TrivialRenderProps extends Component {
state = {
loading: false,
data: []
}
render() {
return this.props.children(this.state)
}
}
这是一个特别设计的例子,但是已经足够了!下面是它的用法:
function ConsumeTrivialRenderProps() {
return <TrivialRenderProps>
{({loading, data}) => {
return <pre>
{`loading: ${loading}`} <br />
{`data: [${data}]`}
</pre>
}}
</TrivialRenderProps>
}
渲染ConsumeTrivialRenderProps 组件, 并且渲染从render props API传过来的 loading 和data 的值。

目前为止,一切都还好!
render props的问题在于它会让你的代码比你想象得更加嵌套。正如前面所说,使用Hooks来重构TrivialRenderProps 组件不是特别困难的事儿。
为了重构它,你只需要实现一个自定义的Hooks,来包裹组件,然后返回之前一样的数据就行。重构完后,就是下面这样:
function ConsumeTrivialRenderProps() {
const { loading, setLoading, data } = useTrivialRenderProps()
return <pre>
{`loading: ${loading}`} <br />
{`data: [${data}]`}
</pre>
}
看起来非常简洁!
下面是自定义的钩子函数useTrivialRenderProps:
function useTrivialRenderProps() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
return {
data,
loading,
}
}
完成!
// before
class TrivialRenderProps extends Component {
state = {
loading: false,
data: []
}
render() {
return this.props.children(this.state)
}
}
// after
function useTrivialRenderProps() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
return {
data,
loading,
}
}
那么这里有什么问题吗?
当你在开发大型的代码库时,你可能会在许多不同的地方使用render props API。把组件的实现方式更改为Hooks,意味着你需要去更改许多不同地方的代码。
那我们可以做一些取舍吗?当然了!
你可以使用Hooks去重构组件,同时也可以返回一个render props API。这样的话,你就可以使用Hooks来重构了,而不至于过多的修改代码。
例如:
// hooks implementation
function useTrivialRenderProps() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
return {
data,
loading,
}
}
// render props implementation
const TrivialRenderProps = ({children, ...props}) => children(useTrivialRenderProps(props));
// export both
export { useTrivialRenderProps };
export default TrivialRenderProps;
现在,通过导出两种实现方式,你可以在你整个代码库里去采用Hooks了。因为不管是之前的实现方式,还是使用Hooks的方式,现在都可以正常工作。
// this will work 👇
function ConsumeTrivialRenderProps() {
return <TrivialRenderProps>
{({loading, data}) => {
return <pre>
{`loading: ${loading}`} <br />
{`data: [${data}]`}
</pre>
}}
</TrivialRenderProps>
}
// so will this 👇
function ConsumeTrivialRenderProps() {
const { loading, setLoading, data } = useTrivialRenderProps()
return <pre>
{`loading: ${loading}`} <br />
{`data: [${data}]`}
</pre>
}
处理状态初始值
在类组件中,一种常见的情景就是,某些状态的初始值是根据某些计算得到的。比如:
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = { token: null }
if (this.props.token) {
this.state.token = this.props.token
} else {
token = window.localStorage.getItem('app-token');
if (token) {
this.state.token = token
}
}
}
}
这是一个可以展示一类问题的简单例子。在组件挂载阶段,是有可能出现,在constructor里经过计算才得到初始值的情况。
在这个例子中,我们判断props是否有传过来token,如果有,就赋值给token;或者是判断本地存储里是否有app-token,如果有并且有值的话,同样的赋值给token。在使用Hooks重构时,如何来处理设置初始值的逻辑?
一个鲜为人知的特性就是,useState钩子可以接受一个initialState 的参数,这个参数也可以是一个函数。
不管这个函数返回了什么,它都会被当作initialState。下面就是经过Hooks重构的组件代码:
function MyComponent(props) {
const [token, setToken] = useState(() => {
if(props.token) {
return props.token
} else {
tokenLocal = window.localStorage.getItem('app-token');
if (tokenLocal) {
return tokenLocal
}
}
})
}
从技术上来说,整个的逻辑是一样的。最主要的地方在于如果你想通过一些逻辑来初始化状态,你可以传一个函数给useState。
最后
使用Hooks来重构你的应用并不是必须得做的。需要通过你自己以及你所在的团队来商量权衡。如果你决定使用Hooks API来重构你的应用,那么我希望这篇文章能给你一些好的建议。
很棒的解读 支持下
还是不太理解 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的优势是非常明显的。