120. 精读《React Hooks 最佳实践》

本文详细介绍了React 16.8版本引入的Hooks特性,包括FunctionComponent的最佳实践,如使用React.FC声明组件类型,React.useMemo优化渲染性能,以及React.useCallback确保函数一致性。同时探讨了组件间通信、状态管理、useEffect注意事项等关键概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

React 16.8 于 2019.2 正式发布,这是一个能提升代码质量和开发效率的特性,笔者就抛砖引玉先列出一些实践点,希望得到大家进一步讨论。

然而需要理解的是,没有一个完美的最佳实践规范,对一个高效团队来说,稳定的规范比合理的规范更重要,因此这套方案只是最佳实践之一。

精读

环境要求

  • 拥有较为稳定且理解函数式编程的前端团队。

  • 开启 ESLint 插件:eslint-plugin-react-hooks。

组件定义

Function Component 采用 const + 箭头函数方式定义:

const App: React.FC<{ title: string }> = ({ title }) => {
return React.useMemo(() => <div>{title}</div>, [title]);
};

App.defaultProps = {
title: 'Function Component'
}

上面的例子包含了:

  1. 用 React.FC 申明 Function Component 组件类型与定义 Props 参数类型。

  2. 用 React.useMemo  优化渲染性能。

  3. 用 App.defaultProps 定义 Props 的默认值。

FAQ

为什么不用 React.memo?

推荐使用 React.useMemo 而不是 React.memo,因为在组件通信时存在 React.useContext 的用法,这种用法会使所有用到的组件重渲染,只有 React.useMemo 能处理这种场景的按需渲染。

没有性能问题的组件也要使用 useMemo 吗?

要,考虑未来维护这个组件的时候,随时可能会通过 useContext 等注入一些数据,这时候谁会想起来添加 useMemo 呢?

为什么不用解构方式代替 defaultProps?

虽然解构方式书写 defaultProps 更优雅,但存在一个硬伤:对于对象类型每次 Rerender 时引用都会变化,这会带来性能问题,因此不要这么做。

局部状态

局部状态有三种,根据常用程度依次排列: useState useRef useReducer 。

useState
const [hide, setHide] = React.useState(false);
const [name, setName] = React.useState('BI');

状态函数名要表意,尽量聚集在一起申明,方便查阅。

useRef
const dom = React.useRef(null);

useRef 尽量少用,大量 Mutable 的数据会影响代码的可维护性。

但对于不需重复初始化的对象推荐使用 useRef 存储,比如 new G2() 。

useReducer

局部状态不推荐使用 useReducer ,会导致函数内部状态过于复杂,难以阅读。 useReducer 建议在多组件间通信时,结合 useContext 一起使用。

FAQ

可以在函数内直接申明普通常量或普通函数吗?

不可以,Function Component 每次渲染都会重新执行,常量推荐放到函数外层避免性能问题,函数推荐使用 useCallback 申明。

函数

所有 Function Component 内函数必须用 React.useCallback 包裹,以保证准确性与性能。

const [hide, setHide] = React.useState(false);

const handleClick = React.useCallback(() => {
setHide(isHide => !isHide)
}, [])

useCallback 第二个参数必须写,eslint-plugin-react-hooks 插件会自动填写依赖项。

发请求

发请求分为操作型发请求与渲染型发请求。

操作型发请求

操作型发请求,作为回调函数:

return React.useMemo(() => {
return (
<div onClick={requestService.addList} />
)
}, [requestService.addList])
渲染型发请求

渲染型发请求在 useAsync 中进行,比如刷新列表页,获取基础信息,或者进行搜索, 都可以抽象为依赖了某些变量,当这些变量变化时要重新取数 :

const { loading, error, value } = useAsync(async () => {
return requestService.freshList(id);
}, [requestService.freshList, id]);

组件间通信

简单的组件间通信使用透传 Props 变量的方式,而频繁组件间通信使用 React.useContext 。

以一个复杂大组件为例,如果组件内部拆分了很多模块, 但需要共享很多内部状态 ,最佳实践如下:

定义组件内共享状态 - store.ts
export const StoreContext = React.createContext<{
state: State;
dispatch: React.Dispatch<Action>;
}>(null)

export interface State {};

export interface Action { type: 'xxx' } | { type: 'yyy' };

export const initState: State = {};

export const reducer: React.Reducer<State, Action> = (state, action) => {
switch (action.type) {
default:
return state;
}
};
根组件注入共享状态 - main.ts
import { StoreContext, reducer, initState } from './store'

const AppProvider: React.FC = props => {
const [state, dispatch] = React.useReducer(reducer, initState);

return React.useMemo(() => (
<StoreContext.Provider value={{ state, dispatch }}>
<App />
</StoreContext.Provider>
), [state, dispatch])
};
任意子组件访问/修改共享状态 - child.ts
import { StoreContext } from './store'

const app: React.FC = () => {
const { state, dispatch } = React.useContext(StoreContext);

return React.useMemo(() => (
<div>{state.name}</div>
), [state.name])
};

如上解决了 多个联系紧密组件模块间便捷共享状态的问题 ,但有时也会遇到需要共享根组件 Props 的问题,这种不可修改的状态不适合一并塞到 StoreContext 里,我们新建一个 PropsContext 注入根组件的 Props:

const PropsContext = React.createContext<Props>(null)

const AppProvider: React.FC<Props> = props => {
return React.useMemo(() => (
<PropsContext.Provider value={props}>
<App />
</PropsContext.Provider>
), [props])
};
结合项目数据流

参考 react-redux hooks。

debounce 优化

比如当输入框频繁输入时,为了保证页面流畅,我们会选择在 onChange 时进行 debounce 。然而在 Function Component 领域中,我们有更优雅的方式实现。

其实在 Input 组件 onChange  使用 debounce 有一个问题,就是当 Input 组件 受控 时, debounce 的值不能及时回填,导致甚至无法输入的问题。

我们站在 Function Component 思维模式下思考这个问题:

  1. React scheduling 通过智能调度系统优化渲染优先级,我们其实不用担心频繁变更状态会导致性能问题。

  2. 如果联动一个文本还觉得慢吗? onChange 本不慢,大部分使用值的组件也不慢,没有必要从 onChange 源头开始就 debounce 。

  3. 找到渲染性能最慢的组件(比如 iframe 组件),对一些频繁导致其渲染的入参进行 useDebounce 。

下面是一个性能很差的组件,引用了变化频繁的 text (这个 text 可能是 onChange 触发改变的),我们利用 useDebounce 将其变更的频率慢下来即可:

const App: React.FC = ({ text }) => {
// 无论 text 变化多快,textDebounce 最多 1 秒修改一次
const textDebounce = useDebounce(text, 1000)

return useMemo(() => {
// 使用 textDebounce,但渲染速度很慢的一堆代码
}, [textDebounce])
};

使用 textDebounce 替代 text 可以将渲染频率控制在我们指定的范围内。

useEffect 注意事项

事实上,useEffect 是最为怪异的 Hook,也是最难使用的 Hook。比如下面这段代码:

useEffect(() => {
props.onChange(props.id)
}, [props.onChange, props.id])

如果 id 变化,则调用 onChange。但如果上层代码并没有对 onChange 进行合理的封装,导致每次刷新引用都会变动,则会产生严重后果。我们假设父级代码是这么写的:

class App {
render() {
return <Child id={this.state.id} onChange={id => this.setState({ id })} />
}
}

这样会导致死循环。虽然看上去 <App> 只是将更新 id 的时机交给了子元素 <Child>,但由于 onChange 函数在每次渲染时都会重新生成,因此引用总是在变化,就会出现一个无限死循环:

新 onChange -> useEffect 依赖更新 -> props.onChange -> 父级重渲染 -> 新 onChange...

想要阻止这个循环的发生,只要改为 onChange={this.handleChange} 即可,useEffect 对外部依赖苛刻的要求,只有在整体项目都注意保持正确的引用时才能优雅生效。

然而被调用处代码怎么写并不受我们控制,这就导致了不规范的父元素可能导致 React Hooks 产生死循环。

因此在使用 useEffect 时要注意调试上下文,注意父级传递的参数引用是否正确,如果引用传递不正确,有两种做法:

  1. 使用 useDeepCompareEffect 对依赖进行深比较。

  2. 使用 useCurrentValue 对引用总是变化的 props 进行包装:

function useCurrentValue<T>(value: T): React.RefObject<T> {
const ref = React.useRef(null);
ref.current = value;
return ref;
}

const App: React.FC = ({ onChange }) => {
const onChangeCurrent = useCurrentValue(onChange)
};

onChangeCurrent 的引用保持不变,但每次都会指向最新的 props.onChange,从而可以规避这个问题。

总结

如果还有补充,欢迎在文末讨论。

如需了解 Function Component 或 Hooks 基础用法,可以参考往期精读:

  • 精读《React Hooks》

  • 精读《怎么用 React Hooks 造轮子》

  • 精读《useEffect 完全指南》

  • 精读《Function Component 入门》


前端公众号严选

这几年公众号暴增,选择一个好的公众号成为了一个难题,以下是我几位朋友的公众号,文章质量信得过。关注公众号,不仅要看文章,还要去了解写公众号的作者,这样才能在一个优秀的圈子里提升自己,可以根据自己的喜好关注,和十几万前端开发一起成长。


640?wx_fmt=png


前端真好玩
640?wx_fmt=jpeg
yck,人称恺哥,掘金前端顶流,正在图灵出版社写第二本书。手握 16000 Star 项目,且被多个知名项目 Merge 过 PR。在公司内部担任面试官,专注深度,对于底层原理以及工程化等等都有独到的见解。
640?wx_fmt=png

前端技术优选
640?wx_fmt=jpeg


前端技术优选为你精选业界前端好文,专注前端node领域,欢迎关注。


640?wx_fmt=png

前端桃园

640?wx_fmt=png

一个有温度的前端号,不止前端,互联网前沿知识个人随想认知提升,学习方法,成为你想成为的那个样子



640?wx_fmt=png


程序员成长指北

640?wx_fmt=png

程序员成长指北 - 完整的 Node.js 技术栈分享(从 JavaScript 到 Node.js ,  再到后端数据库,祝您成为优秀的全栈工程师),并且会定期分享优质前端技术。



640?wx_fmt=png


前端迷

640?wx_fmt=jpeg

「前端迷」公众号是一个公益性的前端技术分享社区,不定期为前端开发者带来面试经验源码解析以及技术分享,欢迎大家订阅。


640?wx_fmt=png


全栈前端精选
640?wx_fmt=png

一线大厂 group 下1.6w+ star开源项目前后端主要开发者。从前端到全栈,每日分享高质量精选文章。欢迎关注!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值