精读《React Hooks 最佳实践》

简介

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 入门》

讨论地址是:精读《React Hooks 最佳实践》 · Issue #202 · dt-fe/weekly

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/713151.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【airtest】自动化入门教程(二)airtest操作

目录 一、touch 二、wait 三、swipe 四、exists 五、text 六、keyevent 七、snapshot 八、sleep 九、断言 9.1 assert_exists 9.2 assert_not_exists 9.3 assert_equal 9.4 assert_not_equal 前言&#xff1a;本文主要针对aritest部分的基础操作,aritest是一个跨平…

网络编程第二天

1.基于TCP的通信(面向连接的通信) 服务器代码实现&#xff1a; #include <myhead.h> #define IP "192.168.126.91" #define PORT 9999 int main(int argc, const char *argv[]) {//1、创建套接字int sfd-1;if((sfdsocket(AF_INET,SOCK_STREAM,0))-1){perror(…

LeetCode 76 最小覆盖字串

LeetCode 76 最小覆盖字串 在本篇博客中&#xff0c;我们将探讨LeetCode上的一道算法题目——“最小覆盖子串”。这道题的主要目标是找到字符串s中包含字符串t中所有字符的最小子串。 问题描述 给定字符串s和t&#xff0c;要求在字符串s中找到一个最小的子串&#xff0c;使得…

5.36 BCC工具之ucalls.py解读

一,工具简介 ucalls工具总结了包括Java、Perl、PHP、Python、Ruby、Tcl和Linux系统调用在内的各种高级语言中的方法调用。它显示最常调用方法的统计信息,以及这些方法的延迟(持续时间)。 通过系统调用支持,ucalls可以提供关于进程与系统交互的基本信息,包括系统调用计数…

ES系列之Logstash实战入门

概述 作为ELK技术栈一员&#xff0c;Logstash用于将数据采集到ES&#xff0c;通过简单配置就能把各种外部数据采集到索引中进行保存&#xff0c;可提高数据采集的效率。 原理 数据源提供的数据进入Logstash的管道后需要经过3个阶段&#xff1a; input&#xff1a;负责抽取数…

C#单向链表实现:在当前节点后插入新数据的方法Insert()

目录 一、涉及到的知识点 1.插入算法 2.示例中current 和 _current 的作用 3.current 和 _current 能否合并为一个变量 4.单向链表节点类的三个属性 &#xff08;1&#xff09;Next属性&#xff1a; &#xff08;2&#xff09; Value属性&#xff1a; &#xff08;3&am…

【ArcPy】批量读取文件夹excel中XY并转为点shp

示例展示 代码 只读取excel中含有XY字段的文件&#xff0c;并将矢量命名为excel文件名称。 import os import pandas as pd import arcpy folder_path r"C:\Users\admin\Desktop\excelfile" extension"xlsx" files [file for file in os.listdir(folder…

SpringCloud gateway限流无效,redis版本低的问题

在使用springCloud gateway的限流功能的时候&#xff0c;配置RedisRateLimiter限流无效&#xff0c;后来发现是Redis版本过低导致的问题&#xff0c;实测 Redis版本为3.0.504时限流无效&#xff0c;改用7.0.x版本的Redis后限流生效。查了资料发现很多人都遇见过这个问题&#x…

RedisTemplate 序列化成功,反序列化失败List, Set, Map失败

RedisTemplate 序列化成功&#xff0c;反序列化失败List, Set, Map失败 异常信息RedisTemplate配置异常原因错误代码示例解决方法 序列化成功&#xff0c;反序列化失败 异常信息 Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve ty…

小程序事件处理

事件处理 一个应用仅仅只有界面展示是不够的&#xff0c;还需要和用户做交互&#xff0c;例如&#xff1a;响应用户的点击、获取用户输入的值等等&#xff0c;在小程序里边&#xff0c;我们就通过编写 JS 脚本文件来处理用户的操作 1. 事件绑定和事件对象 小程序中绑定事件与…

React之组件定义和事件处理

一、组件的分类 在react中&#xff0c;组件分为函数组件和class组件&#xff0c;也就是无状态组件和有状态组件。 * 更过时候我们应该区别使用无状态组件&#xff0c;因为如果有状态组件会触发生命周期所对应的一些函数 * 一旦触发他生命周期的函数&#xff0c;它就会影响当前项…

如何设置从小程序跳转到其它小程序

​有的商家有多个小程序&#xff0c;希望能够通过一个小程序链接到所有其它小程序&#xff0c;用户可以通过点击跳转链接实现从一个小程序跳转到另一个小程序。要怎么才能实现这样的跳转呢。下面具体介绍。 1. 设置跳转。在小程序管理员后台->分类管理&#xff0c;添加一个…

ssm个人学习01

Spring配置文件: spring环境的搭建: 1:导入对应的spring坐标 也就是依赖 2:编写controller, service, dao相关的代码 3:创建配置文件(在resource下面配置文件) 例如:applicationContext.xml <bean id "" class ""> <property name "&…

Node.js 中 fs 模块文件操作的应用教程

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境&#xff0c;它可以让 JavaScript 代码在服务器端运行。在 Node.js 中&#xff0c;fs 模块是用来处理文件系统操作的模块。通过 fs 模块&#xff0c;我们可以进行文件的读取、写入、删除等操作。本教程将介绍如何在 No…

工作电压范围宽的国产音频限幅器D2761用于蓝牙音箱,输出噪声最大仅-90dBV

近年来随着相关技术的不断提升&#xff0c;音箱也逐渐从传统的音箱向智能音箱、无线音箱升级。同时在消费升级的背景下&#xff0c;智能音箱成为人们提升生活品质的方式之一。智能音箱是智能化和语音交互技术的产物&#xff0c;具有点歌、购物、控制智能家居设备等功能&#xf…

python水表识别图像识别深度学习 CNN

python水表识别&#xff0c;图像识别深度学习 CNN&#xff0c;Opencv,Keras 重点&#xff1a;项目和文档是本人近期原创所作&#xff01;程序可以将水表图片里面的数据进行深度学习&#xff0c;提取相关信息训练&#xff0c;lw1.3万字重复15%&#xff0c;可以直接上交那种&…

Vue中<style scoped lang=“scss“>的含义

这段代码中的<style scoped lang"scss">是HTML和Vue框架结合使用时常见的一个模式&#xff0c;具体含义如下&#xff1a; scoped&#xff1a;这是一个Vue.js特有的属性&#xff0c;用来指定样式只应用于当前组件的元素。没有这个属性时&#xff0c;样式会全局应…

python给企微发消息

方法一&#xff1a;webhook方式。使用群机器人给企微群发消息 import requestsdef qwxsendmessage(msg):urlhttps://qyapi.weixin.qq.com/cgi-bin/webhook/send?key6c598840-804a-4eb5-a999-a023313 #url换成自己群机器人的webhookurldata{msgtype:text,text:{content:msg}}…

elasticsearch7.17 terms聚合性能提升90%+

背景 ES7 相比于 ES6 有多个层面的优化&#xff0c;对于开源的ES而言&#xff0c;升级是必经之路。 ES的使用场景非常多&#xff0c;在升级过程中可能会遇到非预期的结果&#xff1b; 比如之前文章提到的典型案例&#xff1a;ES7.17版本terms查询性能问题 ES7.17版本terms查…

【Python笔记-FastAPI】后台任务+WebSocket监控进度

目录 一、代码示例 二、执行说明 (一) 调用任务执行接口 (二) 监控任务进度 实现功能&#xff1a; 注册后台任务&#xff08;如&#xff1a;邮件发送、文件处理等异步场景&#xff0c;不影响接口返回&#xff09;监控后台任务执行进度&#xff08;进度条功能&#xff09;支…