首页 > 其他分享 >初学React useEffect Hook

初学React useEffect Hook

时间:2023-03-21 18:04:11浏览次数:49  
标签:return timeout React Hook varA useState const useEffect

React Hooks 是从功能组件访问 React 的状态和生命周期方法的最佳方式。 ​​useEffect​​​ Hook 是一个在渲染之后和每次 DOM 更新时运行的函数(效果)。在本文中,将讨论一些技巧以更好地使用 ​​useEffect​​ Hook。

通过项目来发现问题,加深对其理解应用到项目中。


开始之前先简单来理解一下 ​​useEffect​​ 设计。

useEffect 设计

React 提供了一个 ​​useEffect​​ 钩子函数来设置在更新后的回调:

const Title = () => {
useEffect(() => {
window.title = "Hello World";
return () => {
window.title = "NoTitle";
};
}, []);
};

​useEffect​​​ 函数采用名为 ​​create​​​ 的回调函数作为其第一个输入参数来定义效果。上面的代码,​​Effect​​​ 在安装组件时将 ​​window.title​​​ 设置为 ​​Hello World​​。

​create​​​ 函数可以返回一个名为 ​​destroy​​​ 的函数来执行清理。这里有趣的是 ​​destroy​​​ 函数由 ​​create​​​ 函数的返回值提供。在前面的示例中,清理将 ​​window.title​​​ 对象在卸载时设置为 ​​NoTitle​​。

​useEffect​​​ 参数列表中的第二个参数是一个名为 ​​deps​​​ 的依赖项数组。如果未设置 ​​deps​​​,则在每次更新期间每次都会调用 ​​Effect​​​,而当给出 ​​deps​​​ 时,​​Effect​​​ 只会在 ​​deps​​ 数组发生更改时调用。


子组件 Effects 优先触发

将 ​​useEffect​​​ Hook 视为 ​​componentDidMount​​​、​​componentDidUpdate​​​ 和 ​​componentWillUnmount​​​ 的组合。所以 ​​useEffect​​ Hook 的行为类似于类生命周期方法。需要注意的一种行为是子回调在父回调之前触发。

function ParentComponent() {
useEffect(() => {
console.log("我是父组件");
});
return <ChildComponent />;
}

function ChildComponent({ fetchProduct }) {
useEffect(() => {
console.log("我是子组件");
});
}

假设必须自动触发付款。这段代码写在 ​​render​​​ 之后运行的子组件中,但是实际支付所需的详细信息(总金额、折扣等)是在父组件的 ​​effect​​ 中获取的。在这种情况下,由于在设置所需的详细信息之前触发了付款,因此就会出现实现逻辑不对。


因此在构建代码的时候需要考虑子组件的 ​​useEffect​​ 会优先执行。


依赖数组

从基础开始。 ​​useEffect​​ Hook 接受第二个参数,称为依赖数组,以控制回调何时触发。

对每个 DOM 更新运行效果

不传递依赖项数组将在每次 DOM 更新时运行回调。

useEffect(() => {
console.log("每次DOM更新时,我都会被调用");
});
在初始渲染上运行效果

传入空数组仅在初始渲染后运行效果。至此,状态已更新为初始值。 DOM 中的进一步更新不会调用此效果。

useEffect(() => {
console.log("我只在初始渲染后被调用一次");
}, []);

这类似于 ​​componentDidMount​​​ 和 ​​componentWillUnmount​​(返回)生命周期方法。这是添加页面所需的所有侦听器和订阅的地方。

对特定 props 变化的运行效果

假设必须根据用户感兴趣的产品来获取数据(产品详细信息),如,所选产品有一个 ​​productId​​​,需要在每次 ​​productId​​ 更改时运行回调——而不仅仅是在初始渲染或每次 DOM 更新时。

useEffect(() => {
getProductDetails(productId);
}, [productId]);

这基本上复制了 ​​componentDidUpdate​​ 生命周期方法。还可以将多个值传递给依赖数组。

一个经典的反例可以帮助更好地理解这一点:

useEffect(() => {
console.log(`当counter1: ${counter1}或counter2: ${counter2}发生变化时,我会被调用。`);
}, [counter1, counter2]);

在上面的示例中,​​counter1​​​ 或 ​​counter2​​ 中的更新将触发。

在依赖数组中传递对象

现在,如果回调依赖是一个对象怎么办。如果这样做,​​effects​​ 会成功运行吗?

const [productId, setProductId] = useState(0);
const [obj, setObj] = useState({ a: 1 });
useEffect(() => {
// 对`obj`的变化做些什么
}, [obj]);

答案是否定的,因为对象是引用类型。对象属性的任何更改都不会被依赖项数组监听到,因为只检查引用而不检查内部的值。

可以遵循几种方法在对象中执行深度比较。

  • ​JSON.stringify​​ 对象:
const [objStringified, setObj] = useState(JSON.stringify({ a: 1 }));
useEffect(() => {
//
}, [objStringified]);

现在,​​useEffect​​ 可以检测到对象的属性何时发生变化并按预期运行。

  • useRef 和 Lodash 进行比较:

还可以编写自定义函数以使用 ​​useRef​​ 进行比较。它用于在组件的当前属性中的整个生命周期中保存可变值。

function deepCompareEquals(prevVal, currentVal) {
return _.isEqual(prevVal, currentVal);
}

function useDeepCompareWithRef(value) {
const ref = useRef();
if (!deepCompareEquals(value, ref.current)) {
ref.current = value;
}

return ref.current;
}

function MyComponent({ obj }) {
useEffect(() => {
//
}, [useDeepCompareWithRef(obj)]);
}
import useDeepCompareEffect from "use-deep-compare-effect";
function MyComponent({ obj }) {
useDeepCompareEffect(() => {}, [obj]);
}

​useDeepCompareEffect​​ 将进行深度比较并仅在对象 obj 更改时运行回调。

将 useEffect 用于单一目的

上面了解了依赖数组,可能需要分离 ​​useEffect​​ 以在组件的不同生命周期事件上运行,或者只是为了更清晰的代码,函数应该服务于单一目的(就像一个句子应该只传达一个想法一样)。

将 ​​useEffects​​​ 拆分为简短单一用途函数可以降低BUG的出现。例如,假设有与 ​​varB​​​ 无关的 ​​varA​​​,并且想要基于 ​​useEffect​​​(带有 ​​setTimeout​​)构建一个递归计数器,先来看一段不推荐的代码:

import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
const [varB, setVarB] = useState(0);

useEffect(() => {
const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);

return () => {
clearTimeout(timeoutA);
clearTimeout(timeoutB);
};
}, [varA, varB]);

return (
<>
<span>
Var A: {varA}, Var B: {varB}
</span>
</>
);
}

上述代码,变量 ​​varA​​​ 和 ​​varB​​​ 中的任何一个更改都会触发两个变量的更新。这就是为什么这个钩子不能正常工作的原因。由于这是一个简短的示例,可能会觉得它很明显,但是,在具有更多代码和变量的较长函数中,会因此错过这一点。所以做正确的事并拆分 ​​useEffect​​ 的逻辑。

import React, { useEffect, useState } from "react";
export default function Home() {
const [varA, setVarA] = useState(0);
const [varB, setVarB] = useState(0);

useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);

return () => clearTimeout(timeout);
}, [varA]);

useEffect(() => {
const timeout = setTimeout(() => setVarB(varB + 2), 2000);

return () => clearTimeout(timeout);
}, [varB]);

return (
<>
<span>
Var A: {varA}, Var B: {varB}
</span>
</>
);
}

上述代码仅为了说明问题,实际编码有些地方可以用其他的方式。

尽可能使用自定义挂钩

再次以上面的例子为例,如果变量 ​​varA​​​ 和 ​​varB​​ 完全独立怎么办?在这种情况下,可以简单地创建一个自定义钩子来隔离每个变量。这样,就可以确切地知道每个函数对哪个变量做了什么。

下面就来构建一些自定义钩子。

import React, { useEffect, useState } from "react";

const useVarA = () => {
const [varA, setVarA] = useState(0);

useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);

return () => clearTimeout(timeout);
}, [varA]);

return [varA, setVarA];
};

const useVarB = () => {
const [varB, setVarB] = useState(0);

useEffect(() => {
const timeout = setTimeout(() => setVarB(varB + 2), 2000);

return () => clearTimeout(timeout);
}, [varB]);

return [varB, setVarB];
};

export default function Home() {
const [varA, setVarA] = useVarA();
const [varB, setVarB] = useVarB();

return (
<>
<span>
Var A: {varA}, Var B: {varB}
</span>
</>
);
}

这样每个变量都有自己的钩子,更易于维护和易于阅读!

有条件地以正确的方式运行 useEffect

关于 ​​setTimeout​​ ,再来看个例子:

import React, { useEffect, useState } from "react";

export default function Home() {
const [varA, setVarA] = useState(0);

useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);

return () => clearTimeout(timeout);
}, [varA]);

return (
<>
<span>Var A: {varA}</span>
</>
);
}

出于某种原因,想将计数器的最大值限制为 ​​5​​。有正确的方法和错误的方法。

先来看看错误的做法:

import React, { useEffect, useState } from "react";

export default function Home() {
const [varA, setVarA] = useState(0);

useEffect(() => {
let timeout;
if (varA < 5) {
timeout = setTimeout(() => setVarA(varA + 1), 1000);
}

return () => clearTimeout(timeout);
}, [varA]);

return (
<>
<span>Var A: {varA}</span>
</>
);
}

虽然这有效,但 ​​clearTimeout​​​ 将在 ​​varA​​​ 发生更改时运行,而 ​​setTimeout​​ 是有条件地运行。

有条件地运行 ​​useEffect​​ 的推荐方法是在函数开头执行条件返回,如下所示:

import React, { useEffect, useState } from "react";

export default function Home() {
const [varA, setVarA] = useState(0);

useEffect(() => {
if (varA >= 5) return;
const timeout = setTimeout(() => setVarA(varA + 1), 1000);

return () => clearTimeout(timeout);
}, [varA]);

return (
<>
<span>Var A: {varA}</span>
</>
);
}

在依赖数组中输入 useEffect 中的每个道具

如果正在使用 ​​ESLint​​​,那么可能已经看到来自 ​​ESLint exhaustive-deps​​​ 规则的警告。这是至关重要的,当应用程序变得越来越大时,每个 ​​useEffect​​​ 中都会添加更多的依赖项(​​props​​)。为了跟踪所有这些并避免陈旧的闭包,应该将每个依赖项添加到依赖项数组中。

同样,关于 ​​setTimeout​​​ 的问题,假设只想运行一次 ​​setTimeout​​​ 并添加到 ​​varA​​:

import React, { useEffect, useState } from "react";

export default function Home() {
const [varA, setVarA] = useState(0);

useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);

return () => clearTimeout(timeout);
}, []); // 避免这种情况:varA 不在依赖数组中!

return (
<>
<span>Var A: {varA}</span>
</>
);
}

虽然上述代码会正确执行,但是如果代码变得更大或者更复杂,可能就会带来问题。在这种情况下,需要将所有变量都映射出来,因为这样可以更容易地测试和检测可能出现的问题(例如过时的 ​​props​​ 和闭包)。

正确的做法应该是:

import React, { useEffect, useState } from "react";

export default function Home() {
const [varA, setVarA] = useState(0);

useEffect(() => {
if (varA > 0) return;
const timeout = setTimeout(() => setVarA(varA + 1), 1000);

return () => clearTimeout(timeout);
}, [varA]);

return (
<>
<span>Var A: {varA}</span>
</>
);
}

总结

上面学习了什么是 ​​useEffect​​​ ?如何更好的使用 ​​useEffect​​​?如果了解基本概念,那么使用 ​​useEffect​​ 就不会有任何问题。学习的一些内容讲通过一个个人项目的形式逐渐完善,丰富功能模块。

项目GITHUB:​​https://github.com/QuintionTang/react-giant​

标签:return,timeout,React,Hook,varA,useState,const,useEffect
From: https://blog.51cto.com/devpoint/6140744

相关文章

  • react循环与条件判断
    react没有语法糖,循环和条件判断都是通过原生js来实现1.条件判断//三元表达式constgetDiv=(flag)=>{return(<div>XXX</div>{flag?<div>条件1</d......
  • react 咻咻咻
    #react必知必会1.函数式组件(简单无状态场景)/class类式组件(复杂有状态场景)2.react思想:状态驱动UI3.组件实例三大核心:refs/props/state4.class=function+hoo......
  • React 限制 Props 和 State 类型
    下面是Component的接口,P代表Props、S代表State。interfaceComponent<P={},S={},SS=any>extendsComponentLifecycle<P,S,SS>{}所以,在tsx中写两个......
  • vue/react关与key的面试题
    虚拟dom中key的作用当状态中的数据发生改变时,react会根据新数据生成新的虚拟dom,随后react进行新虚拟dom与旧虚拟dom的diff算法比较,比较规则如下:1.旧虚拟dom找到了与新......
  • 好客租房44-react组件基础综合案例-5发表评论-1
    发表评论1给按钮绑定点击事件2在事件处理程序中通过state获取评论信息3将评论信息添加到state中并调用setState()方法更新数据//导入reactimportReactfrom'react'imp......
  • 好客租房42-react组件基础综合案例-渲染列表无数据并优化
    渲染列表评论1判断列表数据的长度是否为02如果为0则渲染暂无评论//导入reactimportReactfrom'react'importReactDOMfrom'react-dom'//导入组件//约定1:类组件必......
  • 好客租房41-react组件基础综合案例-渲染列表数据
    1渲染列表在state定义数据进行数据渲染//导入reactimportReactfrom'react'importReactDOMfrom'react-dom'//导入组件//约定1:类组件必须以大写字母开头//约定2:......
  • swagger-ui-react 前端引用草稿
    前言SwaggerUI是一个React组件,它接受一个配置对象作为其唯一的属性。这个配置对象可以包含多个属性来自定义SwaggerUI的行为和外观。以下是SwaggerUI支持的主要属性及其说......
  • react的diff算法
    diff策略React用三大策略将O(n^3)复杂度转化为O(n)复杂度策略一(treediff):WebUI中DOM节点跨层级的移动操作特别少,可以忽略不计。策略二(componentdiff):拥有相......
  • [React] Flushing state updates synchronously with flushSync
    InReact,everyupdateissplitintwophases:During render, Reactcallsyourcomponentstofigureoutwhatshouldbeonthescreen.During commit, React......