谈谈react hooks的优缺点
谈一下个人认为的react hooks
的优缺点,通过和传统的React.Component
进行对比得出。
#优点
一、更容易复用代码
这点应该是react hooks
最大的优点,它通过自定义hooks来复用状态,从而解决了类组件有些时候难以复用逻辑的问题。hooks是怎么解决这个复用的问题呢,具体如下:
- 每调用useHook一次都会生成一份独立的状态,这个没有什么黑魔法,函数每次调用都会开辟一份独立的内存空间。
- 虽然状态(from useState)和副作用(
useEffect
)的存在依赖于组件,但它们可以在组件外部进行定义。这点是class component
做不到的,你无法在外部声明state和副作用(如componentDidMount
)。
上面这两点,高阶组件和renderProps也同样能做到。但hooks实现起来的代码量更少,以及更直观(代码可读性)。
举个例子,经常使用的antd-table,用的时候经常需要维护一些状态 ,并在合适的时机去更改它们:
componentDidMount(){
this.loadData();
}
loadData = ()=>{
this.setState({
current: xxx,
total: xxx,
pageSize: xxx,
dataSource: xxx[]
})
}
onTableChange = ()=>{
this.setState({
current: xxx,
total: xxx,
pageSize: xxx,
})
}
render(){
const {total,pageSize,current,dataSource} = this.state;
return <Table
dataSource={dataSource}
pagination={{total,pageSize,current}
onChange={this.onTableChange}
/>
}
每个table都要写一些这种逻辑,那还有啥时间去摸鱼。这些高度类似的逻辑,可以通过封装一个高阶组件来抽象它们。这个高阶组件自带这些状态,并可以自动调用server去获取remote data。
用高阶组件来实现的话会是这样:
import { Table } from 'antd'
import server from './api'
function useTable(server) {
return function (WrappedComponent) {
return class HighComponent extends React.Component {
state = {
tableProps: xxx,
};
render() {
const { tableProps } = this.state;
return <WrappedComponent tableProps={tableProps} />;
}
};
};
}
@useTable(server)
class App extends Component{
render(){
/**
* 高阶组件/renderProps是通过增强组件的props(赋予一个新的属性或者方法到组件的props属性),
* 实现起来比较隐式。你难以区分这个props是来自哪个高阶组件(特别是使用了较多的高阶组件时),
* 或者还是来自业务的父组件。
*/
const { tableProps } = this.props;
return (
<Table
columns={[...]}
// tableProps包含pagination, onChange, dataSource等属性。
{...tableProps}
/>
)
}
}
如果用hooks来实现的话,会是:
import { Table } from 'antd'
import server from './api'
function useTable(server) {
const [tableProps, setTableProps] = useState(xxx);
return tableProps;
}
function App {
const { tableProps } = useTable();
return (
<Table
columns={[...]}
// tableProps包含pagination, onChange, dataSource等属性
{...tableProps}
/>
)
}
/*
相对比高阶组件“祖父=>父=>子”的层层嵌套,
hooks是这样的:
const { brother1 } = usehook1;
const { brother2} = usehook2;
*/
可以看到,hooks的逻辑更清晰,可读性更好。
二、清爽的代码风格+代码量更少
1. 函数式编程风格,函数式组件、状态保存在运行环境、每个功能都包裹在函数中,整体风格更清爽,更优雅。
2. 对IDE更友好,对比类组件,函数组件里面的unused状态和unused-method更容易被编辑器发现。
3. 使用typescript的话,类型声明也变得更容易。
class Example{
hello: string;
constructor(){
this.hello = 'hello world'
}
}
// 代码量更少
function Example(){
const hello:string = 'hello world'
}
4. 向props或状态取值更加方便,函数组件的取值都从当前作用域直接获取变量,而类组件需要先访问实例this,再访问其属性或者方法,多了一步。
5. 更改状态也变得更加简单, `this.setState({ count:xxx })`变成 `setCount(xxx)`。
因为减少了很多模板代码,特别是小组件写起来更加省事,人们更愿意去拆分组件。而组件粒度越细,被复用的可能性越大。所以,hooks也在不知不觉中改变人们的开发习惯,提高项目的组件复用率。
#缺点
一、响应式的useEffect
写函数组件时,你不得不改变一些写法习惯。你必须清楚代码中useEffect
和useCallback
的“依赖项数组”的改变时机。有时候,你的useEffect依赖某个函数的不可变性,这个函数的不可变性又依赖于另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点意外地被改变了,你的useEffect就被意外地触发了,如果你的useEffect是幂等的操作,可能带来的是性能层次的问题,如果是非幂等,那就糟糕了。
所以,对比componentDidmount
和componentDidUpdate
,useEffect带来的心智负担更大。
二、hooks不擅长异步的代码(旧引用问题)
函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面。当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。比如下面的一个例子(codesandbox):
import React, { useState } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
const onAlertButtonClick = () => {
setTimeout(() => {
alert("Value: " + counter);
}, 3000);
};
return (
<div>
<p>You clicked {counter} times.</p>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
<button onClick={onAlertButtonClick}>
Show me the value in 3 seconds
</button>
</div>
);
};
export default Counter;
当你点击Show me the value in 3 seconds
的后,紧接着点击Click me
使得counter的值从0变成1。三秒后,定时器触发,但alert出来的是0(旧值),但我们希望的结果是当前的状态1。
这个问题在class component
不会出现,因为class component
的属性和方法都存放在一个instance
上,调用方式是:this.state.xxx
和this.method()
。因为每次都是从一个不变的instance
上进行取值,所以不存在引用是旧的问题。
其实解决这个hooks的问题也可以参照类的instance
。用useRef
返回的immutable RefObject
(把值保存在current属性上)来保存state,然后取值方式从counter
变成了: counterRef.current
。如下:
import React, { useState, useRef, useEffect } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
const counterRef = useRef(counter);
const onAlertButtonClick = () => {
setTimeout(() => {
alert("Value: " + counterRef.current);
}, 3000);
};
useEffect(() => {
counterRef.current = counter;
});
return (
<div>
<p>You clicked {counter} times.</p>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
<button onClick={onAlertButtonClick}>
Show me the value in 3 seconds
</button>
</div>
);
};
export default Counter;
结果如我们所期待,alert的是当前的值1。
我们可以把这个过程封装成一个custom hook,如下:
import { useEffect, useRef, useState } from "react";
const useRefState = <T>(
initialValue: T
): [T, React.MutableRefObject<T>, React.Dispatch<React.SetStateAction<T>>] => {
const [state, setState] = useState<T>(initialValue);
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state;
}, [state]);
return [state, stateRef, setState];
};
export default useRefState;
尽管这个问题被巧妙地解决了,但它不优雅、hack味道浓,且丢失了函数编程风格。
三、custom hooks有时严重依赖参数的不可变性
import {useState, useEffect} from 'react'
export function() useData(api){
const [data, setDate] = useState([]);
useEffect(()=>{
api().then(res=>setData(res.data)) ;
// 这里要求传入的api是immutable的,被useCallback/useMemo所包裹。不然每次api一变,
// 都会非预期地多调用一次useEffect。
},[api])
}
对于这点,除了在团队约定参数的不可变性,还可以对useCallback/useMemo进行魔改:
import React from "react";
let useCallback = React.useCallback;
if (__DEV__) {
useCallback = (fn, arr) => {
fn.__useCallback__ = true;
return useCallback(fn, arr);
};
}
export default useCallback;
然后在run-time中去检查是否存在__useCallback__这个属性:
import {useState, useEffect} from 'react'
function checkFn(fn){
if(__DEV__){
if(!fn.__useCallback__){
throw Error('请用团队封装的useCallback来包裹fn')
}
}
}
export function() useData(api){
const [data, setDate] = useState([]);
checkFn(api);
useEffect(()=>{
api().then(res=>setData(res.data)) ;
// 这里要求传入的api是immutable的,被useCallback/useMemo所包裹。不然每次api一变,
// 都会非预期地多调用一次useEffect。
},[api])
}
也有其他的方案:比如用eslint插件去检查。
#怎么避免react hooks的常见问题
- 不要在
useEffect
里面写太多的依赖项,划分这些依赖项成多个单一功能的useEffect
。其实这点是遵循了软件设计的“单一职责模式”。 - 如果你碰到状态不同步的问题,可以考虑下手动传递参数到函数。如:
// showCount的count来自父级作用域
const [count,setCount] = useState(xxx);
function showCount(){ console.log(count) }
// showCount的count来自参数
const [count,setCount] = useState(xxx);
function showCount(c){ console.log(c) }
但这个也只能解决一部分问题,很多时候你不得不使用上述的useRef
方案。
3. 拆分组件,细化组件的粒度。复杂业务场景中使用hooks,应尽可能地细分组件,使得组件的功能尽可能单一,这样的hooks组件更好维护。
#感想
hooks
很好用很强大,但它不擅长异步。但在有太多异步逻辑的代码时,class比它更适合、更稳、更好维护。
原文链接:https://zhuanlan.zhihu.com/p/88593858