useEffect的正确打开方式,useEffect必学必会之入门篇

5,021 阅读5分钟

翻译: 卷帘依旧

原文地址: dmitripavlutin.com/react-useef…

我对React Hooks的丰富功能印象深刻-写很少的代码就可以做很多的事情。

然而hooks的简洁是有代价的-入门相对困难。尤其是useEffect()-这个hook用于在React组件中处理副作用。

在这篇文章中,你会学到如何以及何时使用useEffect()这个hook

1. useEffect()用于处理副作用

React函数式组件借助props和state计算输出。如果函数式组件的目标不是输出一个值,那么这些计算统称为副作用

副作用包括网络请求、直接操作DOM,使用计时器函数(比如setTimeout())等。

组件渲染和副作用的逻辑是相互独立的。直接在组件内部执行副作用是错误行为,因为组件的初始目标就是用于计算输出的。

组件渲染的次数是你无法控制的-如果React需要渲染组件,你是无法阻止的。

function Greet({ name }) {
    const message = `Hello, ${name}!`; // 计算输出
    // Bad!
    document.title = `Greetings to ${name}`; // 副作用!
    return <div>{message}</div>;       // 计算输出
}

那么如何将组件渲染与副作用解耦呢?这时就用到useEffect()了-独立于渲染运行副作用的hook。

import { useEffect } from 'react';
function Greet({ name }) {
  const message = `Hello, ${name}!`;   // 计算输出
  useEffect(() => {
    // Good!
    document.title = `Greetings to ${name}`; // 副作用!
  }, [name]);
  return <div>{message}</div>;         // 计算输出
}

useEffect()接收两个参数:

useEffect(callback, [dependencies])
  • callback是包含副作用逻辑的函数,在每次DOM更新之后callback会执行
  • dependencies是一个可选的依赖数组。useEffect()只有在渲染之间的依赖项发生变化时候才会执行callback

将副作用逻辑放在回调(callback)函数中,然后使用依赖参数(dependencies argument)控制副作用在何时执行。那就是useEffect()的唯一目标。

image.png

比如,在之前的代码片段中你可以看到useEffect()的作用:

useEffect(() => {
    document.title = `Greetings to ${name}`;
}, [name]);

document.title的变化就是副作用,因为它并没有直接计算组件的输出值。那就是文档标题的更新逻辑放在火调中并提供给useEffect()的原因。

另外,不必在每次Greet组件渲染时都更新文档标题,而是只需要在name变化的时候执行-因此才要将name作为依赖传入useEffect(callback, [name])

2. 依赖参数

useEffect(callback, dependencies)的依赖参数允许你控制副作用何时执行。根据依赖参数的不同,有以下3中情况:

  1. 没有传入: 副作用在每次渲染之后都会运行
import { useEffect } from 'react';
function MyComponent() {
  useEffect(() => {
    // Runs after EVERY rendering
  });  
}
  1. 传入空数组[]: 副作用仅在初始渲染之后执行一次
import { useEffect } from 'react';
function MyComponent() {
  useEffect(() => {
    // Runs ONCE after initial rendering
  }, []);
}
  1. 传入propsstate: 任何依赖参数的变化都会引起副作用执行
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
  const [state, setState] = useState('');
  useEffect(() => {
    // Runs ONCE after initial rendering
    // and after every rendering ONLY IF `prop` or `state` changes
  }, [prop, state]);
}

接下来详述第二种和第三种情况,因为这两种在实际中很常用。

3. 组件生命周期

3.1 组件挂载

使用一个空数组作为依赖,副作用仅在组件挂载之后执行一次:

import { useEffect } from 'react';
function Greet({ name }) {
  const message = `Hello, ${name}!`;
  useEffect(() => {
    // Runs once, after mounting
    document.title = 'Greetings page';
  }, []);
  return <div>{message}</div>;
}

useEffect(..., [])中的依赖参数为一个空数组,此时useEffect()中的回调仅在组件完成初始挂载之后执行一次。

尽管name属性的变化导致组件重新渲染,副作用也仅仅在第一次渲染之后执行一次:

// First render
<Greet name="Eric" />   // Side-effect RUNS
// Second render, name prop changes
<Greet name="Stan" />   // Side-effect DOES NOT RUN
// Third render, name prop changes
<Greet name="Butters"/> // Side-effect DOES NOT RUN

在线运行demo

3.2 组件更新

副作用如果用到了propsstate的值,那么必须将这些值作为依赖:

import { useEffect } from 'react';
function MyComponent({ prop }) {
  const [state, setState] = useState();
  useEffect(() => {
    // Side-effect uses `prop` and `state`
  }, [prop, state]);
  return <div>....</div>;
}

在每次渲染更新提交到DOM之后,useEffect(callback, [prop, state])会且仅仅会在依赖数组[prop, state]中的任何值发生变化时,调用callback回调。

通过useEffect()的依赖参数,可以控制何时调用副作用,这与组件的渲染周期无关。这也是useEffect()钩子的本质。

我们在document.title中使用nameprop来改进一下Greet组件:

import { useEffect } from 'react';
function Greet({ name }) {
  const message = `Hello, ${name}!`;
  useEffect(() => {
    document.title = `Greetings to ${name}`; 
  }, [name]);
  return <div>{message}</div>;
}

nameprop作为useEffect(..., [name])的依赖参数,useEffect()钩子在初始渲染之后,以及之后的渲染中如果name值发生变化时,都会执行副作用。

// First render
<Greet name="Eric" />   // Side-effect RUNS
// Second render, name prop changes
<Greet name="Stan" />   // Side-effect RUNS
// Third render, name prop doesn't change
<Greet name="Stan" />   // Side-effect DOES NOT RUN
// Fourth render, name prop changes
<Greet name="Butters"/> // Side-effect RUNS

在线运行demo

4. 副作用清理

有一些需要清理的副作用: 关闭socket,清除计时器等。

如果useEffect(callback, deps)的回调返回的是一个函数,那么useEffect()回认为这是一个副作用清理函数:

useEffect(() => {
    // Side-effect...
    return function cleanup() {
      // Side-effect cleanup...
    };
}, dependencies);

清理的工作流程如下:

  1. 在初始渲染完成之后,useEffect()调用包含副作用的回调,此时清理函数未被调用。
  2. 在之后的渲染中,在下一次副作用回调之前,useEffect()会首先调用上次副作用执行过程中的清理函数(清理前一次副作用执行的产物),然后运行本次副作用。
  3. 最后,在组件卸载之后,useEffect()调用最近一次副作用的清理函数。

image.png

一起来看一个副作用清理生效的例子。

如下组件<RepeatMessage message="My Message" />接收一个propmessage,然后,每2message在控制台打印一次:

import { useEffect } from 'react';
function RepeatMessage({ message }) {
  useEffect(() => {
    setInterval(() => {
      console.log(message);
    }, 2000);
  }, [message]);
  return <div>I'm logging to console "{message}"</div>;
}

在线运行demo

打开样例代码输入一些内容,控制台每2s打印一次输入内容,但我们实际上仅仅需要最新的message。

这是一个清理副作用的例子:取消前一个计时器,然后开始另外一个新的计时器。我们在代码中返回一个停止前一个计时器的清理函数:

import { useEffect } from 'react';
function RepeatMessage({ message }) {
  useEffect(() => {
    const id = setInterval(() => {
      console.log(message);
    }, 2000);
    return () => {
      clearInterval(id);
    };
  }, [message]);
  return <div>I'm logging to console "{message}"</div>;
}

在线运行代码

打开demo,输入一些message:只有最新的message会被打印到控制台

5. useEffect()实际应用

5.1 获取数据

useEffect()能够执行获取数据的副作用。

如下组件FetchEmployees通过网络请求获取员工列表:

import { useEffect, useState } from 'react';
function FetchEmployees() {
  const [employees, setEmployees] = useState([]);
  useEffect(() => {
    async function fetchEmployees() {
      const response = await fetch('/employees');
      const fetchedEmployees = await response.json(response);
      setEmployees(fetchedEmployees);
    }
    fetchEmployees();
  }, []);
  return (
    <div>
      {employees.map(name => <div>{name}</div>)}
    </div>
  );
}

在DOM完成首次挂载之后,useEffect()通过调用fetchEmployees()异步函数发起了一个获取数据的请求。

当请求完成时,setEmployees(fetchedEmployees)用刚刚获取到的员工列表更新了employees的状态。

注意useEffect(callback)中的回调函数callback不能是异步的,但是你可以在回调函数内部先定义异步函数并调用:

function FetchEmployees() {
    const [employees, setEmployees] = useState([]);
    useEffect(() => {  // <--- CANNOT be an async function
      async function fetchEmployees() {
        // ...
      }
      fetchEmployees(); // <--- But CAN invoke async functions
    }, []);
    // ...
}

根据propstate的值运行获取数据的请求,只需要在依赖参数中指出请求所需要的依赖就可以了: useEffect(fetchSideEffect, [prop, stateValue])

6. 结论

useEffect(callback, dependencies)是用于在函数式组件中处理副作用的钩子,callback参数是一个函数,副作用的逻辑放到这个函数内部,dependencies是副作用的依赖列表: 依赖项为propsstate状态值。

useEffect(callback, dependencies)在初始加载完成,或者之后的渲染过程中依赖参数变化之后执行回调。

掌握useEffect()的下一步是理解和避免无限循环陷阱

写在最后

如果你觉得这篇文章对你有用,那就请点个

小小的赞,是我写作的大大动力❤️❤️❤️