React Hooks简要介绍
Hooks是React v16.7.0-alpha中引入的新特性,目前(2018年10月底)还在讨论之中。
关于这次改动,官网里特地表明这不是Breaking Changes,并且向前兼容,大家可以放心地使用。在动机上:
- 使用Hooks将便于开发者拆分和复用state管理的逻辑(而不是state本身)
- 使用Hooks将把Class组件中的React生命周期方法抽象成effects,根据需要插入
- 除了state和生命周期方法,React还将class提供的更多features拆分出来作为额外功能,按需使用
下面将从上面几点分别展开介绍,并给出一些使用须知。
State Hook
根据上面所述,用来拆分和复用state管理逻辑。通常情况下,class组件中的state更新逻辑比较简单。和官网给的例子本质上没什么差别。
1 | import { useState } from 'react'; |
useState
是State Hook提供的API方法,它只需1个入参,表示状态的初始值。返回一个pair:
- 第一个元素,状态本身,类似
this.state.xxx
- 第二个元素,设置状态方法,类似
this.setState({ xxx: 'foo' })
需要注意的是,第二个元素,设置状态的方法不是增量更新,而是直接替换,这点和setState
有区别。
在下面的渲染部分,直接使用状态名即可。当然这里只声明了一个需要的状态变量,需要新的状态变量(比如:[fruit, setFruit]
)时,需要用同样的方法获得返回,像下面这样:
1 | // Declare multiple state variables! |
可以看到,使用State Hook时,如何拆分state到各useState中是需要考虑的事情。
Effect Hook
Effect Hook用来处理React每次渲染完成后的副作用。它等同于componentDidMount
, componentDidUpdate
, 再加上componentWillUnmount
。副作用分两种,需要cleanup和不需要cleanup的。
不需要Cleanup
通常的副作用包括数据请求、DOM修改等等。这些操作不需要清理占用的资源。使用时类似下面这样。
1 | import { useState, useEffect } from 'react'; |
使用useEffect
方法,传入一个函数作为唯一入参。这样,在每次render完成后(包含第一次render),都会执行这个函数,去完成副作用的部分。
你可能有些疑惑,如果我有某个副作用,只在componentDidMount使用一次,比如获取DOM ref这种呢?另外,每次重新渲染后,如果副作用依赖于当前的状态值,难道还需要写if语句判断状态有没有变化吗?接着,往下看。
userEffect
这个方法可以有第二个入参,这个入参是数组类型,表示这个effects所依赖的内部状态。(注意:这个状态必须是上面用useState
声明的)只有数组内的状态变化时,React才会去执行第一个入参的函数。
另外,数组为空时,表示函数没有依赖,即只在componentDidMount时执行一次即可。
1 | useEffect(() => { |
最后,useEffect
是异步完成的,即不会block浏览器更新屏幕内容,以节省资源。在一些不常见的场景,如需要测量当前布局时,需要用同步的useLayoutEffect
。
需要Cleanup
有的副作用以添加事件监听、设置定时器等等的subscription的形式进行,这些在组件销毁后需要释放掉占用的资源,避免内存泄漏。类似你之前在componentWillUnmount
里写的逻辑。
React用useEffect
表示这个副作用的清除操作。用法类似setTimeout
,用返回值作为handler。
1 | useEffect(() => { |
在实际运行时,一个Effect在组件re-render时都会被重新销毁再重建,以便于在componentDidUpdate时,也能跟踪到副作用内使用的状态的最新值。上面那段代码可能会遇到下面这样的实际运行情况:
1 | // Mount with { friend: { id: 100 } } props |
为了避免这样的频繁操作影响性能,可以通过上面介绍的传第二个参数的方式优化性能。官方文档在最后还补充了一句:
In the future, the second argument might get added automatically by a build-time transformation.
一些使用准则
- 在函数内部的最外层使用,别在块语句内使用,以保证正确的内部状态
- 只在函数组件和自定义Hooks中使用Hooks API,以保证可读性
- 这个eslint-plugin能帮助你检查代码风格
为什么会有看起来比较别扭的上面两条规则呢?
从useState
和useEffect
看到,API本身是没有状态的,并不知道API的返回赋值给了哪个变量名。所以,就像介绍里说的:
React relies on the order in which Hooks are called.
React依赖于Hooks的调用顺序,因此在每次render时,Hooks方法的调用顺序一定要保持一致。
(猜测内部用类似数组的结构保存了一个函数组件内的多个Hooks)
从而,所有导致Hooks可能不按一致顺序执行的写法都不建议使用。为了保证Hooks执行顺序所见即所得,又有了第二条准则。
组合 - 自定义Hooks
Hooks除了或多或少基于React提供的Hooks外,只是再普通不过的JavaScript function而已。可以将组件中共用的状态逻辑拆分出来作为自定义Hooks。类似下面这样:
1 | import { useState, useEffect } from 'react'; |
强烈建议用户自定义的Hooks函数也以use
开头。在使用时,就像使用正常的函数即可。
1 | function FriendStatus(props) { |
Hooks共用的是状态逻辑,使用同一个自定义Hooks创建的状态是相互隔离的。
你可以发挥你的想象力,抽象共用的状态逻辑,使用组合的方式(在函数中组合,React并不建议在class组件中使用mixin)构建新组件,减少组件代码长度。
官网举了个非常简单却普遍的useReducer
的例子。
1 | function useReducer(reducer, initialState) { |
还有什么Hooks
useContext
,接受React.createContext
作为入参,在每次provider更新后,自动用最新的context重渲染。useReducer
,组件状态逻辑很复杂时,代替useState
使用useCallback
,保存一个和当前状态相关的函数,只有状态变化时,函数才会更新,避免重复创建函数。1
2
3
4
5
6const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b]
);useMemo
,保存一个和当前状态相关的值,只有状态变化时,值才会重新计算。不提供数组代表每次渲染都会更新。useRef
,获取DOM ref1
2
3
4
5
6
7
8
9
10
11
12
13function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}useImperativeMethods
,暴露当前组件Ref给父组件useMutationEffect
和useLayoutEffect
,类似useEffect
只是同步执行,而且执行时机有区别
更多参考文档介绍
还有问题?
- 不常见的方法如
componentDidCatch
未来会支持 - 范式上,Hooks是高阶组件的更简单形式
- 借助
useMemo
,shouldComponentUpdate
可以有更好的写法 this.foo
这种写法可以用useRef
实现 - 个人感觉有点不符合直觉- state split准则:将一起改变的state split到一起
useEffect
需要用到prevState,prevProps时需要hack- 使用Hooks不会更慢,反而会更快
- idea来源
实践
刚好最近在某管理后台需要有用户列表页的展示,除了获取数据的副作用,只有渲染的功能,用Hooks实现起来就很自然,而在原来的范式下,因为一个额外的网络请求,就需要把functional组件转成class,随之而来的又是一系列的模板代码和声明周期函数。
使用Hooks之后的代码像下面这样:
1 | // ... |
使用原来的模式时,大概像下面这样:
1 | class UserList extends Component { |
虽然代码行数类似,但是代码信噪比和可拓展性明显上面更优。
感受与展望
我理解Hooks的目的并不是强行把class组件里的功能硬塞到functional组件里(虽然从用法上确实有这种感觉),推荐使用funcational组件的形式。而是一次新的复用组件逻辑方式的尝试。毕竟组合函数比组合class简单多了(React并不建议mixin)。同时通过上面的简单实践可以发现,使用Hooks之后,少了许多Spaghetti code,看上去清爽了许多,可读性也随着提高。
不过另一方面,Hooks的API初看上去挺美,挺简洁好用,那是因为最开始举例的场景简单,不需要hack。由于使用Hooks就意味着用全盘用function的形式写组件,原来用class写法写的复杂的业务组件,如果都用Hooks的方式写,也需要开发者具有一定的设计模式意识。同时在有些场景(比如上面说的prevState,prevProps)要使用比较反直觉的操作才能完成。期待后面Hooks API不断优化后的结果。
在逐渐使用Hooks的方式写组件后,业务中会有一些共用的Hooks抽象出来,整个项目目录结构也会发生变化,Hooks文件的管理方式还要再实践。期待Hooks能让每个模块代码都能小于200行的一天更早到来😌。