【React】Hooks 从入门到放弃
zh-rocco opened this issue · comments
【React】Hooks 从入门到放弃
Hook
是React 16.8
的新增特性。它可以让你在不编写class
的情况下使用state
以及其他的React
特性。
背景
在 Hooks 出现之前, 函数组件对比类组件(class)形式有很多局限, 例如:
- 不能使用 state、ref 等属性, 只能通过函数传参的方式使用 props
- 没有生命周期钩子
对比类组件
类组件缺点:
- 在复杂组件中, 耦合的逻辑代码很难分离
- 组件间逻辑复用困难
- 监听清理和资源释放问题, 同一资源的生成和销毁逻辑不在一起, 被分割在
mount
和unmount
钩子中 - class 学习成本, 比如
this
指向问题
Hooks 缺点
- 依赖传染性, 这导致了开发复杂性的提高、可维护性的降低
- 缓存雪崩, 这导致运行性能的降低
- 异步任务下无法批量更新, 闭包问题
运行过程对比
类组件 | 函数组件 | |
---|---|---|
第一次执行 | class 实例化 -> 执行 render 函数 | 执行函数 -> 初始化 useState |
后续执行 | 重新执行 render 函数 | 重新执行函数 -> 从 useState 中获取状态 |
Hooks 注意事项
- 只能在函数组件或
custom Hook
的最外层调用Hooks
, 不要在循环、条件判断、事件处理器中调用 - 不要在类组件和其他
JavaScript
函数中调用 - 不要在
useMemo
,useReducer
,useEffect
内调用Hooks
闭包问题
一些好的实践
state
拆分与分组
粒度过细
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);
粒度过粗
const [state, setState] = useState({
width: 100,
height: 100,
left: 0,
top: 0,
});
Good
const usePosition = () => {
const [position, setPosition] = useState({ left: 0, top: 0 });
useEffect(() => {
// ...
}, []);
return [position, setPosition];
};
const Box = () => {
const [position, setPosition] = usePosition();
const [size, setSize] = useState({ width: 100, height: 100 });
// ...
};
使用 useState
时, 我们需要考虑状态拆分的粒度问题, 如果粒度过细, 代码就会变得冗余, 并且在更新 state
时会频繁触发函数组件的 re-render
, 如果粒度过粗, 代码的可复用性就会降低。
建议
- 将完全不相关的
state
拆分为多组state
- 比如
size
和position
- 比如
- 如果某些
state
是相互关联的, 或者需要一起发生改变, 就可以把它们合并为一组state
- 比如
left
和top
- 比如
dependency
数量优化
如果发现依赖数组依赖过多, 我们就需要重新审视自己的代码
- 依赖数组依赖的值最好不要超过
3
个, 否则会导致代码会难以维护 - 如果发现依赖数组依赖的值过多, 我们应该采取一些方法来减少它
- 去掉不必要的依赖
- 将
Hook
拆分为更小的单元, 每个Hook
依赖于各自的依赖数组 - 通过合并相关的
state
, 将多个依赖值聚合为一个 - 通过
setState
回调函数获取最新的state
, 以减少外部依赖 - 通过
ref
来读取可变变量的值, 不过需要注意控制修改它的途径
该不该使用 useMemo
对于这个问题, 有的人从来没有思考过, 有的人甚至不觉得这是个问题, 因为我们在网上经常见到的做法是, 那就是不管什么情况, 只要用 useMemo 或者 useCallback 简单的『包裹一下』, 似乎就能使应用远离性能的问题, 但真的是这样吗?有的时候 useMemo 没有任何作用, 甚至还会影响应用的性能
useMemo
适用的场景
- 有些计算开销很大, 我们就需要『记住』它的返回值, 避免每次
render
都去重新计算- 比如
cloneDeep
一个很大并且层级很深的数据
- 比如
- 由于值的引用发生变化, 导致下游组件重新渲染, 我们也需要『记住』这个值
- 对于组件内部用到的
Object
、Array
、Function
等, 如果用在了其他Hook
的依赖数组中, 或者作为props
传递给了下游组件, 应该使用useMemo
- 自定义
Hook
中暴露出来的Object
、Array
、Function
等, 都应该使用useMemo
, 以确保当值相同时, 引用不发生变化
- 对于组件内部用到的
有一个误区就是对创建函数开销的评估, 有的人觉得在 render
中创建函数可能会开销比较大, 为了避免函数多次创建, 使用了 useMemo
或者 useCallback
, 但是对于现代浏览器来说, 创建函数的成本微乎其微
如果因为
prop
的值相同而引用不同, 从而导致子组件发生re-render
, 不一定会造成性能问题, 因为Virtual DOM re-render
并不等同于DOM re-render
, 但是当子组件特别大时,Virtual DOM
的Diff
开销也很大, 因此还是应该尽量避免子组件re-render
最后的最后, 我们再来将上面涉及到的内容整体的回顾一下
- 将完全不相关的 state 拆分为多组 state
- 如果某些 state 是相互关联的, 或者需要一起发生改变, 就可以把它们合并为一组 state
- 依赖数组的值最好不要过多, 如果发现依赖数组依赖的值过多, 我们应该采取一些方法来减少它
- 去掉不必要的依赖
- 将 Hook 拆分为更小的单元, 每个 Hook 依赖于各自的依赖数组
- 通过合并相关的 state, 将多个依赖值聚合为一个
- 通过 setState 回调函数获取最新的 state, 以减少外部依赖
- 通过 ref 来读取可变变量的值, 不过需要注意控制修改它的途径
- 应该使用 useMemo 的场景
- 保持引用相等
- 成本很高的计算
- 无需使用 useMemo 的场景
- 如果返回的值是原始值, 一般不需要使用 useMemo
- 仅在组件内部用到的 object、array、函数等(没有作为 props 传递给子组件), 且没有用到其他 Hook 的依赖数组中, 一般不需要使用 useMemo
- 若 Hook 类型相同, 且依赖数组一致时, 应该合并成一个 Hook
- 自定义 Hook 的返回值可以使用 Tuple 类型, 更易于在外部重命名, 如果返回的值过多, 则不建议使用
- ref 不要直接暴露给外部使用, 而是提供一个修改值的方法
- 在使用 useMemo 或者 useCallback 时, 可以借助 ref 或者 setState callback, 确保返回的函数只创建一次, 也就是说, 函数不会根据依赖数组的变化而二次创建
强大的 useEffect
摆脱类组件的思维模式
当使用生命周期钩子时,我们需要手动去判断哪些数据(dataRange)发生了变化,然后更新到对应的数据(data)。
而在 Hooks 的使用中,我们只需关注哪些值(dataRange)需要进行同步。
const Chart = ({ dateRange }) => {
const [data, setData] = useState();
useEffect(() => {
const newData = getDataWithinRange(dateRange);
setData(newData);
}, [dateRange]);
return <svg className="Chart" />;
};
- 使用 useEffect 进行数据的处理;
- 存储变量到 state;
- 在 JSX 中引用 state。
我们不需要使用 state ,那是类组件的开发模式,因为在类组件中,render 函数和生命周期钩子并不是在同一个函数作用域下执行,所以需要 state 进行中间的存储,同时执行的 setState 让 render 函数再次执行,借此获取最新的 state。
而在函数式组件中我们有时根本不会需要用到 state 这样的状态存储,我们仅仅是想使用。
const Chart = ({ dateRange }) => {
const data = useMemo(() => getDataWithinRange(dateRange), [dateRange]);
return <svg className="Chart" />;
};
因为函数组件中 render 和生命周期钩子在同一个函数作用域中,这也就意味着不再需要 state 作中间数据桥梁,我们可以直接在函数执行时获取到处理的数据,然后在 return 的 JSX 中使用,不必需要每次使用属性都要在 state 中声明和创建了,不再需要重新渲染执行一次函数(setData)了,所以我们去除掉了 useState。这样,我就减少了一个 state 的声明以及一次重新渲染。
我们把变量定义在函数里面,而不是定义在 state 中,这是类组件由于其结构和作用域上与函数组件相比的不足,是函数组件的优越性。
避坑指南
- 需要注意的是, 我们在使用
ref
的过程当中要特别小心, 因为它可以随意赋值, 所以一定要控制好修改它的方法, 特别是一些底层模块, 在封装的时候千万不要直接暴露ref
, 而是提供一些修改它的方法
感受一下 Hook 的优势
类组件:
class Chart extends Component {
state = {
data: null,
dimensions: null,
xScale: null,
yScale: null,
};
componentDidMount() {
const newData = getDataWithinRange(this.props.dateRange);
this.setState({ data: newData });
this.setState({ dimensions: getDimensions() });
this.setState({ xScale: getXScale() });
this.setState({ yScale: getYScale() });
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.dateRange != this.props.dateRange) {
const newData = getDataWithinRange(this.props.dateRange);
this.setState({ data: newData });
}
if (prevProps.margins != this.props.margins) {
this.setState({ dimensions: getDimensions() });
}
if (prevState.data != this.state.data) {
this.setState({ xScale: getXScale() });
this.setState({ yScale: getYScale() });
}
}
render() {
return <svg className="Chart" />;
}
}
Hooks:
const Chart = ({ dateRange, margins }) => {
const data = useMemo(() => getDataWithinRange(dateRange), [dateRange]);
const dimensions = useMemo(getDimensions, [margins]);
const xScale = useMemo(getXScale, [data]);
const yScale = useMemo(getYScale, [data]);
return <svg className="Chart" />;
};