开工福利|免费学 2200+ 精品线上课,企业成员人人可得! 了解详情
写点什么

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:053556

评论

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

InfoQ 极客传媒 15 周年庆征文|一文读懂分布式系统本质:高吞吐、高可用、可扩展

No Silver Bullet

架构 分布式系统 可扩展 6月月更 InfoQ极客传媒15周年庆

【Spring 学习笔记(一)】第一个Spring程序与IoC思想

倔强的牛角

6月月更

当AI抄起了水表

华为云开发者联盟

人工智能 modelarts workflow 智能水务

关于fastjson出现反序列化远程代码执行漏洞的通知

天翼云开发者社区

选择天翼云混合云管理平台的五大理由

天翼云开发者社区

龙蜥开发者说:不忘初心,方得始终 | 第 7 期

OpenAnolis小助手

开源 cpu 龙蜥开发者说 飞腾 不忘初心

天猫精灵语音技能单轮对话表达式的参数定义

汪子熙

人工智能 机器学习 聊天机器人 机器人 6月月更

【高并发】在高并发环境下该如何构建应用级缓存?

冰河

并发编程 多线程 高并发 异步编程 6月月更

Hoo研究院|一文速览风投机构NGC Ventures的加密布局

区块链前沿News

投资 VC Hoo NGC

撑算力之帆,天翼云助力数字时代逐潮者远航

天翼云开发者社区

Java 中三大类数据类型

迷篱

这个API文档,太拽了吧!

Liam

前端 Postman API API文档 开放api

你的App是信息化时代的还是数字化时代的

FinClip

小程序开发

使用 LakeSoul 构建实时机器学习样本库

Geek_a02d1e

机器学习 大数据 开源 新基建 湖仓一体

函数节流和函数防抖和他们的区别

工边页字

JavaScript 性能优化 前端 6月月更

【云服务器】云计算平台的架构是什么样的?

Finovy Cloud

云服务器 GPU服务器

玩转云端|一文读懂天翼云CDN升级重点

天翼云开发者社区

Flutter 图片库重磅开源!

阿里巴巴终端技术

flutter 开源 native 客户端

直播回顾 | 7000字干货,深析区块链+汽车供应链金融的应用价值

旺链科技

区块链 产业区块链 供应链金融

移动平台打造新生态 | 助力企业跨业务、一站式、全场景的系统建设

BeeWorks

征文投稿丨基于轻量应用服务器搭建Hexo个人博客

阿里云弹性计算

node.js nginx git Hexo 轻量应用服务器

数字先锋| 天翼云牵手中能融合

天翼云开发者社区

AI“爷青回”:一键找回童年记忆

最新动态

样品管理系统解决方案

低代码小观

Lims LIMS实验室信息管理系统 LIMS系统

2022年中国新能源汽车换电市场发展洞察

易观分析

新能源汽车

leetcode 417. Pacific Atlantic Water Flow 太平洋大西洋水流问题

okokabcd

LeetCode 搜索 数据结构与算法

多模态语义检索 | 基于 MetaSpore 快速部署 HuggingFace 预训练模型

Geek_a02d1e

机器学习 深度学习 开源 AI 多模态

博云容器云产品族:如何实现让“Any APP on Any Kubernetes”?

BoCloud博云

云原生 容器云

哈希游戏开发竞猜系统哈希值hash算法

薇電13242772558

哈希算法

【LeetCode】 删除二叉搜索树中的节点Java题解

Albert

LeetCode 6月月更

5G+实时云渲染:交互实时云看车革新购车体验

3DCAT实时渲染

5G 汽车之家 汽车 元宇宙 实时云渲染

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