热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

React useEffect使用教程

React useEffect使用教程-目录一、每一次渲染都有它自己的PropsandState二、每次渲染都有它自己的Effects三、关于依赖项不要对React撒谎四、两种诚实

这篇文章会假设你对useEffectAPI有一定程度的了解。

一、每一次渲染都有它自己的 Props and State

在我们讨论 effects 之前,我们需要先讨论一下渲染,当我们更新 state 的时候,React会重新渲染组件。每一次渲染都能拿到独立的 state,这个状态值是函数中的一个常量。

这里关键的点在于任意一次渲染中的常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的值独立于其他渲染。

如果 props 和 state 在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的值。

二、每次渲染都有它自己的Effects

让我们先看向官网的 useEffect 的例子:

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  return (
    

You clicked {count} times

); }

effect是如何读取到最新的count状态值的呢?

也许,是某种 watching 机制类似 vue 中的数据响应式使得能够在 effect 函数内更新?也或许是一个可变的值,React 会在我们组件内部修改它以使我们的 effect 函数总能拿到最新的值?

都不是。

我们已经知道是某个特定渲染中的常量。事件处理函数“看到”的是属于它那次特定渲染中的状态值。对于 effects 也同样如此:

并不是count的值在“不变”的 effect 中发生了改变,而是 effect 函数本身在每一次渲染中都不相同。

React 会记住你提供的 effect 函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它。

所以虽然我们说的是一个 effect(这里指更新document的title),但其实每次渲染都是一个不同的函数— 并且每个 effect 函数看到的 props 和 state 都来自于它属于的那次特定渲染。

三、关于依赖项不要对React撒谎

现在只需要记住:如果你设置了依赖项,effect 中用到的所有组件内的值都要包含在依赖中。这包括props,state,函数组件内的任何东西。

在下面这个组件中,我们的直觉是:“开启一次定时器,清除也是一次”。直觉上我们会设置依赖为 '[]'。“我只想运行一次 effect ”,但是这样对吗?

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return {count};
}

我们以为他会一直递增下去,但实际上他只会递增一次,你想要触发一次因为它是定时器 ,但为什么会有问题?

在第一次渲染中我们执行了setCount(0 + 1)。但是我们设置了[]依赖,effect不会再重新运行,它后面每一秒都会调用setCount(0 + 1)。我们对 React 撒谎说我们的 effect 不依赖组件内的任何值,可实际上我们的 effect 有依赖。

四、两种诚实告知依赖的方法

第一种策略是在依赖中包含所有 effect 中用到的组件内的值。让我们在依赖中包含:count

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

这在我们大部分初级开发者的眼中都没有什么问题,并且程序确实不会出任何 bug,现在,每次修改都会重新运行 effect,这能解决问题但是我们的定时器会在每一次改变后清除和重新设定。这肯定不是我们想要的结果。

第二种策略是修改 effect 内部的代码以确保它包含的值只会在需要的时候发生变更。

在这个场景中,我们其实并不需要在effect中使用 count。当我们想要根据前一个状态更新状态的时候,我们可以使用的函数形式:

useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

我们需要告知React的仅仅是去递增状态 - 不管它现在具体是什么值。注意我们做到了移除依赖,并且没有撒谎。我们的 effect 不再读取渲染中的count值。

五、来自useReducer的助攻

如果我们有两个互相依赖的状态,或者我们想基于一个 prop 来计算下一次的 state,setCount(c => c + 1)它并不能做到。幸运的是,有一个更强大的姐妹模式,它的名字叫useReducer。

我们先来修改上面的例子让它包含两个状态:count 和 step 。我们的定时器会每次在 count 上增加一个 step 值:

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);
  return (
    <>
      {count}
       setStep(Number(e.target.value))} />
    
  );
}

注意我们没有撒谎。既然我们在 effect 里使用了step,我们就把它加到依赖里。所以这也是为什么代码能运行正确。

这个例子目前的行为是修改会重启定时器 - 因为它是依赖项之一。在大多数场景下,这正是你所需要的。清除上一次的effect然后重新运行新的effect并没有任何错。不过,假如我们不想在改变后重启定时器,我们该如何从effect中移除对的依赖呢?

下面这句话我希望你作为一名 react 开发人员要记下来:

当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。

reducer 可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。

我们用一个dispatch依赖去替换 effect 的依赖 step

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  return (
    <>
      {count}
       {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    
  );
}
const initialState = {
  count: 0,
  step: 1,
};
function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}

你可能会问:“这怎么就更好了?”答案是React会保证dispatch在组件的声明周期内保持不变。所以上面例子中不再需要重新订阅定时器。

相比于直接在 effect 里面读取状态,它 dispatch 了一个action来描述发生了什么。这使得我们的 effect 和状态解耦。我们的 effect 不再关心怎么更新状态,它只负责告诉我们发生了什么。更新的逻辑全都交由 reducer 去统一处理。

六、把函数移到Effects里

一个典型的误解是认为函数不应该成为依赖。举个例子,下面的代码看上去可以运行正常:

function SearchResults() {
  const [data, setData] = useState({ hits: [] });
  async function fetchData() {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=react',
    );
    setData(result.data);
  }
  useEffect(() => {
    fetchData();
  }, []); 
  // ...

需要明确的是,上面的代码可以正常工作。但这样做在组件日渐复杂的迭代过程中我们很难确保它在各种情况下还能正常运行。

如果我们在某些函数内使用了某些 state 或者 prop:

function SearchResults() {
  const [query, setQuery] = useState('react');
  // Imagine this function is also long
  function getFetchUrl() {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }
  // Imagine this function is also long
  async function fetchData() {
    const result = await axios(getFetchUrl());
    setData(result.data);
  }
  useEffect(() => {
    fetchData();
  }, []);
  // ...
}

如果我们忘记去更新使用这些函数(很可能通过其他函数调用)的effects的依赖,我们的effects就不会同步props和state带来的变更。这当然不是我们想要的。

如果某些函数仅在effect中调用,你可以把它们的定义移到effect中:

function SearchResults() {
  // ...
  useEffect(() => {
    // We moved these functions inside!
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=react';
    }
    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }
    fetchData();
  }, []); 
}

这么做有什么好处呢?我们不再需要去考虑这些“间接依赖”。我们的依赖数组也不再撒谎:在我们的 effect 中确实没有再使用组件范围内的任何东西。

如果我们后面修改getFetchUrl去使用状态 query,我们更可能会意识到我们正在effect里面编辑它因此,我们需要把 query添加到effect的依赖里:

function SearchResults() {
  const [query, setQuery] = useState('react');
  useEffect(() => {
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=' + query;
    }
    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }
    fetchData();
  }, [query]); 
}

七、我不想把可复用的函数放到Effect里

有时候你可能不想把函数移入 effect 里。比如,组件内有几个 effect 使用了相同的函数,你不想在每个 effect 里复制黏贴一遍这个逻辑。也或许这个函数是一个 prop。

在这种情况下你应该忽略对函数的依赖吗?这么做是不对的。再次强调,effects不应该对它的依赖撒谎。通常我们还有更好的解决办法。一个常见的误解是,“函数从来不会改变”。但是这篇文章你读到现在,你知道这显然不是事实。实际上,在组件内定义的函数每一次渲染都在变。

function SearchResults() {
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []);
  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []);
}

在这个例子中,你可能不想把getFetchUrl移到 effects 中,因为你想复用逻辑。

另一方面,如果你对依赖很“诚实”,你可能会掉到陷阱里。我们的两个 effects 都依赖 getFetchUrl,而它每次渲染都不同,所以我们的依赖数组会变得无用:

function SearchResults() {
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); 
  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]);
  // ...
}

我们有两个更简单的解决办法。

第一个, 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在 effects 中使用:

function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []);
  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); 
  // ...
}

你不再需要把它设为依赖,因为它们不在渲染范围内,因此不会被数据流影响。

或者, 你也可以把它包装成useCallback Hook:

function SearchResults() {
  const getFetchUrl = useCallback((query) => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]);
  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]);
  // ...
}

我们用 useCallback 对 getFetchUrl 做了一层缓存,现在只有当依赖项变化的时候,才会重新执行 useCallback 来返回新的函数,依赖项没有变化的时候就算组件 rerender 了,这个函数也不会重新执行,这样我们把 getFetchUrl 作为 useEffect 的依赖就没问题了。

不同于传递参数的方式,现在我们从状态中读取 query:

function SearchResults() {
  const [query, setQuery] = useState('react');
  const getFetchUrl = useCallback(() => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, [query]);  
  useEffect(() => {
    const url = getFetchUrl();
    // ... Fetch data and do something ...
  }, [getFetchUrl]);
  // ...
}

如果query保持不变,useCallback也会保持不变,我们的 effect 也不会重新运行。但是如果修改了 query,useCallback 也会随之改变,因此会重新请求数据。这就像你在Excel里修改了一个单元格的值,另一个使用它的单元格会自动重新计算一样。


推荐阅读
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • Spring – Bean Life Cycle
    Spring – Bean Life Cycle ... [详细]
  • 本文介绍了几种常用的图像相似度对比方法,包括直方图方法、图像模板匹配、PSNR峰值信噪比、SSIM结构相似性和感知哈希算法。每种方法都有其优缺点,适用于不同的应用场景。 ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 本文对比了杜甫《喜晴》的两种英文翻译版本:a. Pleased with Sunny Weather 和 b. Rejoicing in Clearing Weather。a 版由 alexcwlin 翻译并经 Adam Lam 编辑,b 版则由哈佛大学的宇文所安教授 (Prof. Stephen Owen) 翻译。 ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 在软件开发过程中,经常需要将多个项目或模块进行集成和调试,尤其是当项目依赖于第三方开源库(如Cordova、CocoaPods)时。本文介绍了如何在Xcode中高效地进行多项目联合调试,分享了一些实用的技巧和最佳实践,帮助开发者解决常见的调试难题,提高开发效率。 ... [详细]
  • 本文详细解析了使用C++实现的键盘输入记录程序的源代码,该程序在Windows应用程序开发中具有很高的实用价值。键盘记录功能不仅在远程控制软件中广泛应用,还为开发者提供了强大的调试和监控工具。通过具体实例,本文深入探讨了C++键盘记录程序的设计与实现,适合需要相关技术的开发者参考。 ... [详细]
  • 在对WordPress Duplicator插件0.4.4版本的安全评估中,发现其存在跨站脚本(XSS)攻击漏洞。此漏洞可能被利用进行恶意操作,建议用户及时更新至最新版本以确保系统安全。测试方法仅限于安全研究和教学目的,使用时需自行承担风险。漏洞编号:HTB23162。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 深入解析 Android 中 EditText 的 getLayoutParams 方法及其代码应用实例 ... [详细]
  • 技术日志:使用 Ruby 爬虫抓取拉勾网职位数据并生成词云分析报告
    技术日志:使用 Ruby 爬虫抓取拉勾网职位数据并生成词云分析报告 ... [详细]
  • Android中将独立SO库封装进JAR包并实现SO库的加载与调用
    在Android开发中,将独立的SO库封装进JAR包并实现其加载与调用是一个常见的需求。本文详细介绍了如何将SO库嵌入到JAR包中,并确保在外部应用调用该JAR包时能够正确加载和使用这些SO库。通过这种方式,开发者可以更方便地管理和分发包含原生代码的库文件,提高开发效率和代码复用性。文章还探讨了常见的问题及其解决方案,帮助开发者避免在实际应用中遇到的坑。 ... [详细]
  • 计算机视觉领域介绍 | 自然语言驱动的跨模态行人重识别前沿技术综述(上篇)
    本文介绍了计算机视觉领域的最新进展,特别是自然语言驱动的跨模态行人重识别技术。上篇内容详细探讨了该领域的基础理论、关键技术及当前的研究热点,为读者提供了全面的概述。 ... [详细]
  • 今天我开始学习Flutter,并在Android Studio 3.5.3中创建了一个新的Flutter项目。然而,在首次尝试运行时遇到了问题,Gradle任务 `assembleDebug` 执行失败,退出状态码为1。经过初步排查,发现可能是由于依赖项配置不当或Gradle版本不兼容导致的。为了解决这个问题,我计划检查项目的 `build.gradle` 文件,确保所有依赖项和插件版本都符合要求,并尝试更新Gradle版本。此外,还将验证环境变量配置是否正确,以确保开发环境的稳定性。 ... [详细]
author-avatar
昕谊凡
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有