likes
comments
collection
share

精读React hook(五):useEffect使用细节知多少?

作者站长头像
站长
· 阅读数 15

概念解释

当我们谈论 React,我们通常在讨论纯函数组件,这意味着它们没有副作用。但在实际应用中,我们通常需要执行有副作用的操作,如数据获取、手动更改 DOM、设置订阅等。useEffect就是为此设计的。

useEffect的定义如下:

useEffect(setup, dependencies?)

useEffect接受两个参数:

  1. setup 函数:这是包含我们的 Effect 逻辑的函数。setup 函数里还可以选择返回一个cleanup函数,cleanup函数会在组件卸载的时候执行。
  2. 依赖数组(optional dependencies) :这是 setup 代码内部引用的所有响应式值的列表。响应式值包括 props、state 以及直接在组件主体中声明的所有变量和函数。依赖数组可以不传递、传空数组和非空数组。

用法详解

基础使用

最简单的用法,在useEffect函数组件中执行副作用(例如:数据获取、手动修改 DOM、订阅事件等)。

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

function Example() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        document.title = `点击了 ${count} 次`;
    });

    return (
        <div>
            <p>点击了 {count} 次</p>
            <button onClick={() => setCount(count + 1)}>点击我</button>
        </div>
    );
}

不同的依赖数组的区别

现在升级上面这个示例,来看看不同**依赖数组(optional dependencies)**有什么区别

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

function Example() {
		const [leftCount, setLeftCount] = useState(0);
	  const [rightCount, setRightCount] = useState(0);

    useEffect(() => {
	    /**
	     * This useEffect will be executed when:
	     * 1、the component is mounted and unmounted.
	     * 2、click left button.
	     * 3、click right button.
	     */
	    console.log(`Button left clicked ${leftCount} times`);
	    document.title = `Button left clicked ${leftCount} times`;
	  });

		useEffect(() => {
		    /**
		     * This useEffect will be executed when:
		     * 1、the component is mounted and unmounted.
		     * 2、click right button.
		     */
		    console.log(`Button right clicked ${rightCount} times`);
		  }, [rightCount]);
		
		  useEffect(() => {
		    /**
		     * This useEffect will be executed when:
		     * 1、the component is mounted and unmounted.
		     */
		    console.log(`I have a empty array of dependencies.`);
		  }, []);

    return (
        <div className="flex">
		<div>
	            <p>左按钮点击了 {leftCount} 次</p>
	            <button onClick={() => setLeftCount(count + 1)}>左按钮</button>
		</div>
		<div>
	            <p>右按钮点击了 {rightCount} 次</p>
	            <button onClick={() => setRightCount(count + 1)}>右按钮</button>
		</div>
        </div>
    );
}

示例代码 TypeScript 版已发布到我的Github,演示Demo已发布到我的示例站

这个示例中,我们写了3个useEffect,添加了不同的依赖数组,它们分别会有这样的表现:

  • 第一个useEffect没有添加依赖数组,它的触发时机有:

    • 组件挂载、卸载的时候
    • 页面每一次re-render的时候,即leftCountrightCount更新的时候,也是左按钮和右按钮点击的时候
  • 第二个useEffect依赖数组添加了rightCount,它的触发时机有:

    • 组件挂载、卸载的时候
    • rightCount触发的re-render的时候,即rightCount更新的时候,也是右按钮点击的时候
  • 第三个useEffect依赖数组说空的,它的触发时机有:

    • 组件挂载、卸载的时候

综上,我们只应该在必要的时候给useEffect添加依赖项,这样可以避免一些不必要的重新渲染。

这个示例也说明了,在单个组件中允许使用多个useEffect,我们可以按照不同的关注点将副作用逻辑分开。

清除函数的作用

开篇我们还提到一个cleanup函数,cleanup函数只在组件卸载时执行,它的用法如下:

useEffect(() => {
    ……

    return () => {
        // 组件卸载会执行这里面的代码
    };
}, []);

应用场景1:清除定时器

如果你在useEffect中设置了一个定时器(如setTimeoutsetInterval),在组件卸载前,你应该清除它,以防止它在组件不在 DOM 中时仍然执行。

useEffect(() => {
    const timer = setInterval(() => {
        console.log('console once per second.');
    }, 1000);
    
    return () => {
        clearInterval(timer);
    };
}, []);

应用场景2:事件监听器

为全局对象(如windowdocument)或特定 DOM 元素添加的事件监听器应当在组件卸载或不再需要它们时被移除。

useEffect(() => {
    const handleResize = () => {
        console.log("Window resized!");
    };

    window.addEventListener('resize', handleResize);
    
    return () => {
        window.removeEventListener('resize', handleResize);
    };
}, []);

应用场景3:WebSockets聊天室

当使用 WebSockets 或其他实时通信技术时,你可能会在组件加载时建立一个连接,而在组件卸载时需要断开这个连接。

useEffect(() => {
    const socket = new WebSocket('ws://weijunext.com/socket');

    socket.onmessage = (event) => {
        console.log(event.data);
    };
    
    return () => {
        socket.close();
    };
}, []);

应用场景4:请求的取消

对于可能在组件卸载后才完成的异步请求(如使用 Axios 发起的 HTTP 请求),你应该在组件卸载前取消它们,以防止设置已卸载组件的状态。

useEffect(() => {
    const source = axios.CancelToken.source();

    axios.get('/api/some-endpoint', { cancelToken: source.token })
        .then(response => {
            console.log(response.data);
        })
        .catch(error => {
            if (axios.isCancel(error)) {
                console.log('Request cancelled');
            } else {
                console.error(error);
            }
        });
    
    return () => {
        source.cancel('Component unmounted');
    };
}, []);

应用场景5:DOM 直接操作和第三方 DOM 库

如果使用了直接操作 DOM 的方法或使用了如 D3、animation 这样的第三方库来操作 DOM,你可能需要在组件卸载时清理或还原某些操作。

useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(1000);
    return () => {
      animation.stop();
    };
  }, []);

跳过初始渲染

在某些情况下,当组件首次渲染时,我们不希望立即执行某些操作。这些操作可能包括发送网络请求、触发某些动画或其他任务。而是只有在某个值或依赖项发生变化后,我们才希望执行这些任务。

假设我们有一个count状态,我们希望当count值发生变化时显示一个通知,但不希望在组件首次加载时显示这个通知。

import React, { useState, useEffect, useRef } from 'react';

function Counter() {
    const [count, setCount] = useState(0);
    const mounted = useRef(false);

    useEffect(() => {
        if (mounted.current) {
            alert('Count value changed!');
        } else {
            mounted.current = true;
        }
    }, [count]);

    return (
        <div>
            <button onClick={() => setCount(count + 1)}>Increase</button>
            <p>Count: {count}</p>
        </div>
    );
}

export default Counter;

这个示例中,当用户点击 Increase 按钮增加count值时,会出现一个通知。但是在页面首次加载时,不会有任何通知出现。

结语

useEffect是一个常用的 hook,正因为常用容易让开发者忽略了其中的使用细节,本文主要针对用法细节进行梳理,希望对大家有帮助。

系列文章列表

未完待续……