NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

React Hooks 踩坑分享

  • 2020-05-29
  • 本文字数:5848 字

    阅读完需:约 19 分钟

React Hooks踩坑分享

前言:React Hooks 被越来越多的人认可,整个社区都以积极的态度去拥抱它。在最近的一段时间笔者也开始在一些项目中尝试去使用 React Hooks。原本以为 React Hooks 很简单,和类组件差不多,看看 API 就能用起来了。结果在使用中遇到了各种各样的坑,通过阅读 React Hooks 相关的文章发现 React Hooks 和类组件有很多不同。由此,想和大家做一些分享。


如果要在项目中使用 React Hooks,强烈推荐先安装eslint-plugin-react-hooks(由 React 官方发布)。在很多时候,这个 eslint 插件在我们使用 React Hooks 的过程中,会帮我们避免很多问题。


本文主要讲以下内容:


  1. 函数式组件和类组件的不同

  2. React Hooks 依赖数组的工作方式

  3. 如何在 React Hooks 中获取数据

一、函数式组件和类组件的不同

React Hooks 由于是函数式组件,在异步操作或者使用 useCallBack、useEffect、useMemo 等 API 时会形成闭包。


先看一下以下例子。在点击了展示现在的值按钮三秒后,会 alert 点击次数:


function Demo() {  const [num, setNum] = useState(0);
const handleClick = () => { setTimeout(() => { alert(num); }, 3000); };
return ( <div> <div>当前点击了{num}次</div> <button onClick={() => { setNum(num + 1) }}>点我</button> <button onClick={handleClick}>展示现在的值</button> </div> );};
复制代码



我们按照下面的步骤去操作:


  • 点击num到 3

  • 点击展示现在的值按钮

  • 在定时器回调触发之前,点击增加num到 5。


可以猜一下 alert 会弹出什么?


分割线




其最后弹出的数据是 3。



为什么会出现这样的情况,最后的num不是应该是 5 吗?


上面例子中,num仅是一个数字而已。 它不是神奇的“data binding”, “watcher”, “proxy”,或者其他任何东西。它就是一个普通的数字像下面这个一样:


const num = 0;// ...setTimeout(() => {  alert(num);}, 3000);// ...
复制代码


我们组件第一次渲染的时候,从useState()拿到num的初始值为 0,当我们调用setNum(1),React 会再次渲染组件,这一次num是 1。如此等等:


// 第一次渲染function Demo() {  const num = 0; // 从useState()获取  // ...  setTimeout(() => {    alert(num);  }, 3000);  // ...}
// 在点击了一次按钮之后function Demo() { const num = 1; // 从useState()获取 // ... setTimeout(() => { alert(num); }, 3000); // ...}

// 又一次点击按钮之后function Demo() { const num = 2; // 从useState()获取 // ... setTimeout(() => { alert(num); }, 3000); // ...}
复制代码


在我们更新状态之后,React 会重新渲染组件。每一次渲染都能拿到独立的num状态,这个状态值是函数中的一个常量。


所以在num为 3 时,我们点击了展示现在的值按钮,就相当于:


function Demo() {  // ...  setTimeout(() => {    alert(3);  }, 3000)  // ...}
复制代码


即便 num 的值被点击到了 5。但是触发点击事件时,捕获到的num值为 3。




上面的功能,我们尝试用类组件实现一遍:


class Demo extends Component {  state = {    num: 0,  }
handleClick = () => { setTimeout(() => { alert(this.state.num); }, 3000); }
render() { const { num } = this.state; return ( <div> <p>当前点击了{num}次</p> <button onClick={() => { this.setState({ num: num + 1 }) }}>点击</button> <button onClick={this.handleClick}>展示现在的值</button> </div> ); }};
复制代码


我们按照之前同样的步骤去操作:


  • 点击num到 3

  • 点击展示现在的值按钮

  • 在定时器回调触发之前,点击增加num到 5



这一次弹出的数据是 5。


为什么同样的例子在类组件会有这样的表现呢?


我们可以仔细看一下 handleClick 方法:


handleClick = () => {  setTimeout(() => {    alert(this.state.num);  }, 3000)}
复制代码


这个类方法从 this.state.num 中读取数据,在 React 中 state 是不可变的。然而,this 是可变的。


通过类组件的this,我们可以获取到最新的 state 和 props。


所以如果在用户再点击了展示现在的值按钮的情况下我们对点击按钮又点击了几次,this.state将会改变。handleClick方法从一个“过于新”的state中得到了num


这样就引起了一个问题,如果说我们 UI 在概念上是当前应用状态的一个函数,那么事件处理程序和视觉输出都应该是渲染结果的一部分。我们的事件处理程序应该有一个特定的 props 和 state


然而在类组件中,我们通过this.state读取的数据并不能保证其是一个特定的 state。handleClick事件处理程序并没有与任何一个特定的渲染绑定在一起。


从上面的例子,我们可以看出 React Hooks 在某一个特定渲染中 state 和 props 是与其相绑定的,然而类组件并不是。

二、React Hooks 依赖数组的工作方式

在 React Hooks 提供的很多 API 都有遵循依赖数组的工作方式,比如 useCallBack、useEffect、useMemo 等等。


使用了这类 API,其传入的函数、数据等等都会被缓存。被缓存的内容其依赖的 props、state 等值就像上面的例子一样都是“不变”的。只有当依赖数组中的依赖发生变化,它才会被重新创建,得到最新的 props、state。所以在用这类 API 时我们要特别注意,在依赖数组内一定要填入依赖的 props、state 等值。


这里给大家举一个反例:


function Demo() {  const [num, setNum] = useState(0);
const handleClick = useCallback(() => { setNum(num + 1); }, []);
return ( <div> <p>当前点击了{num}次</p> <button onClick={handleClick}>点击</button> </div> );}
复制代码


useCallback本质上是添加了一层依赖检查。当我们函数本身只在需要的时候才改变。


在上面的例子中,我们无论点击多少次点击按钮,num的值始终为 1。这是因为useCallback中的函数被缓存了,其依赖数组为空数组,传入其中的函数会被一直缓存。


handleClick其实一直都是:


const handleClick = () => {    setNum(0 + 1);};
复制代码


即便函数再次更新,num的值变为 1,但是 React 并不知道你的函数中依赖了num,需要去更新函数。


唯有在依赖数组中传入了num,React 才会知道你依赖了num,在num的值改变时,需要更新函数。


function Demo() {  const [num, setNum] = useState(0);
const handleClick = useCallback(() => { setNum(num + 1); }, [num]); // 添加依赖num
return ( <div> <p>当前点击了{num}次</p> <button onClick={handleClick}>点击</button> </div> );};
复制代码


点击点击按钮,num 的值不断增加。


(其实这些归根究底,就是 React Hooks 会形成闭包)

三、如何在 React Hooks 中获取数据

在我们用习惯了类组件模式,我们在用 React Hooks 中获取数据时,一般刚开始大家都会这么写吧:


function Demo(props) {  const { query } = props;  const [list, setList] = useState([]);
const fetchData = async () => { const res = await axios(`/getList?query=${query}`); setList(res); };
useEffect(() => { fetchData(); // 这样不安全(调用的fetchData函数使用了query) }, []);
return ( <ul> {list.map(({ text }) => { return ( <li key={text}>{ text }</li> ); })} </ul> );};
复制代码


其实这样是不推荐的一种模式,要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:


function Demo(props) {  const { query } = props;  const [list, setList] = useState([]);
useEffect(() => { const fetchData = async () => { const res = await axios(`/getList?query=${query}`); setList(res); };
fetchData(); }, [query]);
return ( <ul> {list.map(({ text }) => { return ( <li key={text}>{ text }</li> ); })} </ul> );};
复制代码


但是如果你在不止一个地方用到了这个函数或者别的原因,你无法把一个函数移动到 effect 内部,还有一些其他办法:


  • 如果这函数不依赖 state、props 内部的变量。可以把这个函数移动到你的组件之外。这样就不用其出现在依赖列表中了。

  • 如果其不依赖 state、props。但是依赖内部变量,可以将其在 effect 之外调用它,并让 effect 依赖于它的返回值。

  • 万不得已的情况下,你可以把函数加入 effect 的依赖项,但把它的定义包裹进useCallBack。这就确保了它不随渲染而改变,除非它自身的依赖发生了改变。


另外一方面,业务一旦变的复杂,在 React Hooks 中用类组件那种方式获取数据也会有别的问题。


我们做这样一个假设,一个请求入参依赖于两个状态分别是 query 和 id。然而 id 的值需要异步获取(只要获取一次,就可以在这个组件卸载之前一直用),query 的值从 props 传入:


function Demo(props) {  const { query } = props;  const [id, setId] = useState();  const [list, setList] = useState([]);
const fetchData = async (newId) => { const myId = newId || id; if (!myId) { return; } const res = await axios(`/getList?id=${myId}&query=${query}`); setList(res); };
const fetchId = async () => { const res = await axios('/getId'); return res; };
useEffect(() => { fetchId().then(id => { setId(id); fetchData(id); }); }, []);
useEffect(() => { fetchData(); }, [query]);
return ( <ul> {list.map(({ text }) => { return ( <li key={text}>{ text }</li> ); })} </ul> );};
复制代码


在这里,当我们的依赖的query在异步获取id期间变了,最后请求的入参,其query将会用之前的值。(引起这个问题的原因还是闭包,这里就不再复述了)


对于从后端获取数据,我们应该用 React Hooks 的方式去获取。这是一种关注数据流和同步思维的方式。


对于刚才这个例子,我们可以这样解决:


function Demo(props) {  const { query } = props;  const [id, setId] = useState();  const [list, setList] = useState([]);
useEffect(() => { const fetchId = async () => { const res = await axios('/getId'); setId(res); };
fetchId(); }, []);
useEffect(() => { const fetchData = async () => { const res = await axios(`/getList?id=${id}&query=${query}`); setList(res); }; if (id) { fetchData(); } }, [id, query]);
return ( <ul> {list.map(({ text }) => { return ( <li key={text}>{ text }</li> ); })} </ul> );}
复制代码


一方面这种方式可以让我们的代码更加清晰,一眼就能看明白获取这个接口的数据依赖了哪些 state、props,让我们更多的去关注数据流的改变。另外一方面也避免了闭包可能会引起的问题。


但是同步思维的方式也会有一些坑,比如这样的场景,有一个列表,这个列表可以通过子元素的按钮增加数据:


function Children(props) {  const { fetchData } = props;
return ( <div> <button onClick={() => { fetchData(); }}>点击</button> </div> );};
function Demo() { const [list, setList] = useState([]);
const fetchData = useCallback(async () => { const res = await axios(`/getList`); setList([...list, ...res]); }, [list]);
useEffect(() => { fetchData(); }, [fetchData]);
return ( <div> <ul> {list.map(({ text }) => { return ( <li key={text}>{ text }</li> ); })} </ul> <Children fetchData={fetchData} /> </div> );};
复制代码


这种场景下,会一直加载数据,造成死循环。


每次调用fetchData函数会更新listlist更新后fetchData函数就会被更新。fetchData更新后useEffect会被调用,useEffect中又调用了fetchData函数。fetchData被调用导致list更新…


当出现这种 根据前一个状态更新状态 的时候,我们可以用 useReducer 去替换 useState:


function Children(props) {  const { fetchData } = props;
return ( <div> <button onClick={() => { fetchData(); }}>点击</button> </div> );};
const initialList = [];
function reducer(state, action) { switch (action.type) { case 'increment': return [...state, ...action.payload]; default: throw new Error(); }}
export default function Demo() { const [list, dispatch] = useReducer(reducer, initialList);
const fetchData = useCallback(async () => { const res = await axios(`/getList`); dispatch({ type: 'increment', payload: res }); }, []);
useEffect(() => { fetchData(); }, [fetchData]);
return ( <div> <ul> {list.map(({ text }) => { return ( <li key={text}>{ text }</li> ); })} </ul> <Children fetchData={fetchData} /> </div> );};
复制代码


React 会保证dispatch在组件的声明周期内保持不变。所以上面的例子中不需要依赖dispatch


用了useReducer我们就可以移除list依赖。不会再出现死循环的情况。


通过 dispatch 了一个 action 来描述发生了什么。这使得我们的fetchData函数和list状态解耦。我们的fetchData函数不再关心怎么更新状态,它只负责告诉我们发生了什么。更新的逻辑全都交由 reducer 去统一处理。


(我们使用函数式更新也能解决这个问题,但是更推荐使用 useReducer)


在某些场景下useReducer会比 useState 更适用。例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数。


如果大家遇到其它的一些复杂场景,用上面介绍的方法无法解决。那就试试用 useRef 吧。


文章如有疏漏、错误欢迎批评指正。


作者介绍


本文转载自公众号有赞 coder(ID:youzan_coder)。


原文链接


https://mp.weixin.qq.com/s?__biz=MzAxOTY5MDMxNA==&mid=2455760831&idx=1&sn=8a83cc2bacba8044fe1a0e6cc5ac498d&chksm=8c68699abb1fe08c997a74e6e51691a09b02e3f0b6e28995bf88cf6701bf73ea17eb642ffe6e&scene=27#wechat_redirect


2020-05-29 10:053394

评论

发布
暂无评论
发现更多内容

保姆级教程!玩转 ChunJun 详细指南

袋鼠云数栈

大数据 开源

BGA焊接问题解析,华秋一文带你读懂

华秋电子

mysql-online-ddl是否需要rebuild

TiDB 社区干货传送门

AutoCAD安装失败,提示错误“Error 112”和安装进度条倒退为0

互联网搬砖工作者

【v6 认证】PCTA/PCTP/PCSD 我的备考经验

TiDB 社区干货传送门

社区活动 新版本/特性发布 TUG 话题探讨 6.x 实践

Nautilus Chain主网上线在即空投规则公布,如何获得更多的空投?

西柚子

Chaosd 模拟两地三中心集群的网络环境

TiDB 社区干货传送门

实践案例 管理与运维 故障排查/诊断 安装 & 部署

从反脆弱角度说一说:技术系统高可用性策略

小小怪下士

Java 程序员 系统设计 后端 秒杀

多平台小程序一站式管理分享

FinClip

对tidb-lightning导入机制的一点点研究

TiDB 社区干货传送门

故障排查/诊断 TiDB 源码解读

如何在树莓派上使用MQTT协议

EMQ映云科技

树莓派 物联网 IoT mqtt 企业号 3 月 PK 榜

SSO认证是什么意思?有哪些优势?

行云管家

SSO认证

实用性好的云管平台有哪些?咨询电话多少?

行云管家

云计算 云资源 云管理

Docker等容器技术如何与移动开发相结合

FinClip

Web前端设计开发工具集(JS框架、CSS预处理)

2D3D前端可视化开发

前端开发 代码编辑器 css预处理器 web前端开发 前端开发工具

接招吧!最强“高并发”系统设计 46 连问,分分钟秒杀一众面试者

做梦都在改BUG

Java 系统设计 高并发

从零开始自己动手写阻塞队列

Java你猿哥

Java 线程 阻塞队列 实战

如何使用ShareSDK快速实现Android APP的社会化分享登录功能

MobTech袤博科技

二本4年Java经验,五面阿里艰苦经历(定薪45K),回馈一波心得体会

Java你猿哥

Java redis 面试 Spring Boot 面经

MobPush iOS SDK API

MobTech袤博科技

Nautilus Chain主网上线在即空投规则公布,如何获得更多的空投?

威廉META

2023年,LED显示屏配套设备急需升级和优化

Dylan

产品 制造 LED显示屏

深入理解Spring注解机制:注解的搜索与处理机制

Java你猿哥

Java spring Spring Boot ssm Spring注解

TiCDC+Confluent同步数据到Oracle

TiDB 社区干货传送门

实践案例 集群管理 管理与运维 安装 & 部署 数据库架构设计

喜讯!华秋电子荣登“2022年中国产业互联网百强企业”榜单

华秋电子

2023年GitHub爆火的spring全家桶进阶笔记震撼开源!

开心学Java

Java spring springboot spring-cloud

重磅消息!ChatGPT 联网,Milvus & Zilliz 正式嵌入用于向量检索!

Zilliz

非结构化数据 Milvus Zilliz 向量数据库 ChatGPT

修复SSH在 MacOS Ventura 系统上不能使用RSA签名的问题

互联网搬砖工作者

夺冠在即!2022 OceanBase数据库大赛12强集结

OceanBase 数据库

数据库 oceanbase

可观测性之谷歌性能主管最新的有关LCP的文章

Yestodorrow

性能 可观测性 用户体验

中国音乐市场增长超28%,首次成为全球第五大市场

HIFIVE音加加

知识产权 娱乐 音乐 版权 娱乐产业

React Hooks踩坑分享_移动_苏木_InfoQ精选文章