React 借鉴了许多函数式编程的理念,使得它在构建复杂用户界面时能够保持代码的简洁、可维护和可测试性。函数式编程在很大程度上依赖于纯函数

纯函数(Pure Function)是指在计算机科学和函数式编程中使用的一类函数。一个函数如果具备以下两个主要特性,我们就可以称之为纯函数:

  1. 引用透明性:纯函数在相同的输入下总是产生相同的输出。也就是说,函数的输出仅依赖于其输入参数,不依赖于任何外部状态或变量。
  2. 无副作用:纯函数不会改变函数外部的任何状态或变量,即在函数执行过程中,不会产生任何影响外部环境的副作用,如更改全局变量、输出日志、修改输入参数等。

纯函数的这两个特性为代码的测试、调试、并行化和重构提供了很大的方便。由于纯函数的输出完全由输入决定,预测函数的行为变得更加简单。另外,由于没有副作用,纯函数之间不会互相影响,这有助于提高代码的可维护性和可读性。

在理想模式下 React 组件应该是纯函数,有些组件需要与外部系统同步。例如根据 React state 控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志,在 React 中被称为副作用,它们是“额外”发生的事情,与渲染过程无关

编写 Effect

编写 Effect 需要遵循以下三个步骤:

  1. 声明 Effect,默认情况下 Effect 会在每次提交后都会执行
  2. 指定 Effect 依赖,大多数 Effect 应该按需执行,而不是在每次渲染后都执行
  3. 必要时添加清理函数,组件卸载或者依赖项改变时候,在执行新的 Effect 之前调用
 1import React, { useEffect } from 'react';
 2
 3const MyComponent = () => {
 4  useEffect(() => {
 5    
 6    console.log('Component mounted or updated');
 7
 8    
 9    return () => {
10      console.log('Component will unmount or cleanup before next effect');
11    };
12  }, []); 
13  
14  return <div>Hello, World!</div>;
15};

合理的使用依赖

减少或者合理使用 useEffect 的依赖项可以减少组件重新渲染次数,显著优化性能

使用 useMemo 或 useCallback

 1import React, { useEffect, useMemo, useState } from 'react';
 2
 3const MyComponent = ({ items }) => {
 4  const [filteredItems, setFilteredItems] = useState([]);
 5
 6  
 7  const filteredItemsMemo = useMemo(() => {
 8    return items.filter(item => item.value > 10);
 9  }, [items]);
10
11  useEffect(() => {
12    
13    setFilteredItems(filteredItemsMemo);
14  }, [filteredItemsMemo]);
15
16  return (
17    <div>
18      {filteredItems.map(item => (
19        <div key={item.id}>{item.name}</div>
20      ))}
21    </div>
22  );
23};
24
25export default MyComponent;

使用函数更新状态

 1const [count, setCount] = useState(0);
 2
 3useEffect(() => {
 4 
 5 const id = setInterval(() => {
 6   setCount(prevCount => prevCount + 1);  
 7 }, 1000);
 8
 9 return () => clearInterval(id);
10}, []);

使用稳定的 dispatch 函数

useReducer 创建的 dispatch 函数是一个闭包,它持有对内部状态和 reducer 函数的引用,这些引用在创建 dispatch 时就固定了。因为这些引用不会变,所以 dispatch 函数也不会变

也就是说 dispatch 函数在组件的整个生命周期内是同一个引用,即使组件重新渲染 dispatch 引用也保持不变,利用这一点可以把原本依赖 state 的 useEffect 改成依赖稳定的 dispatch 函数

 1const MyComponent = () => {
 2  const [state, dispatch] = useReducer(...);
 3
 4  useEffect(() => {
 5    const fetchData = async () => {
 6      dispatch({ type: 'FETCH_INIT' });
 7
 8      try {
 9        const response = await fetch('https://api.example.com/data');
10        const result = await response.json();
11        dispatch({ type: 'FETCH_SUCCESS', payload: result });
12      } catch (error) {
13        dispatch({ type: 'FETCH_FAILURE', payload: error });
14      }
15    };
16
17    fetchData();
18  }, [dispatch]);
19
20  const { data, loading, error } = state;
21
22  if (loading) {
23    return <div>Loading...</div>;
24  }
25
26  if (error) {
27    return <div>Error: {error.message}</div>;
28  }
29
30  return (
31    <div>
32      {data && data.map(item => (
33        <div key={item.id}>{item.name}</div>
34      ))}
35    </div>
36  );
37};

清理函数的作用

useEffect 的清理函数有两个主要的执行时机

  1. 组件卸载时:当组件即将从 DOM 中被移除时,清理函数会被调用。这个过程有助于避免内存泄漏和其他资源泄露。例如如果你在 useEffect 中设置了订阅、计时器或者事件监听器,在组件卸载时需要清理这些资源
  2. 依赖项变化时:如果 useEffect 的依赖项数组中有一个或多个依赖项发生变化,旧的副作用会先被清理,然后再运行新的副作用函数

清理函数通过管理资源、避免重复操作、取消异步操作等方式,确保 React 组件在其生命周期内的行为是可控和高效的

防止内存泄漏

当组件卸载时,清理函数会释放任何仍在占用的资源或内存,确保不会发生内存泄漏。例如清除定时器、取消订阅网络请求或 WebSocket 连接等

 1useEffect(() => {
 2  const interval = setInterval(() => {
 3    console.log('Interval running');
 4  }, 1000);
 5  
 6  return () => {
 7    clearInterval(interval); 
 8  };
 9}, []);

避免重复操作

在组件重新渲染时,清理函数可以中断或取消之前的副作用,避免重复执行同样的副作用操作

 1useEffect(() => {
 2  const handleResize = () => {
 3    console.log('Window resized');
 4  };
 5
 6  window.addEventListener('resize', handleResize);
 7  
 8  return () => {
 9    window.removeEventListener('resize', handleResize); 
10  };
11}, []);

协调异步操作

清理函数可以在组件卸载时取消未完成的异步操作,比如网络请求,以防止在组件不再需要时更新它的状态

 1useEffect(() => {
 2  const fetchData = async () => {
 3    const controller = new AbortController();
 4    const signal = controller.signal;
 5    
 6    try {
 7      const response = await fetch('https://api.example.com/data', { signal });
 8      const data = await response.json();
 9      console.log(data);
10    } catch (error) {
11      if (error.name !== 'AbortError') {
12        console.error('Fetch error:', error);
13      }
14    }
15    
16    return () => {
17      
18      controller.abort();
19    };
20  };
21  
22  fetchData();
23}, []);

useLayoutEffect

React 组件更新到页面有几个过程

  1. React 组件渲染阶段,生成新的虚拟 DOM,计算出浏览器需要做的更新
  2. React 向浏览器提交 DOM 变更阶段
  3. 浏览器 DOM 树构建、更新
  4. 样式计算和布局,浏览器绘制页面

useEffect 在浏览器完成渲染之后异步地调用执行,而很多时候需要对浏览器渲染结果立刻做出改变,这样会造成页面两次渲染。useLayoutEffect 可以保证在所有浏览器 DOM 变更之后、浏览器重新绘制之前执行(也就是上面流程这种 3 和 4 之间)

这意味着 useLayoutEffect 内的代码会在浏览器刷新屏幕之前执行,可以同步读取布局信息,并可通过 DOM API 修改布局来避免用户看到中间状态的闪烁或不一致

 1import React, { useLayoutEffect, useRef, useState } from 'react';
 2
 3const Example = () => {
 4  const [height, setHeight] = useState(0);
 5  const divRef = useRef(null);
 6
 7  useLayoutEffect(() => {
 8    
 9    if (divRef.current) {
10      setHeight(divRef.current.getBoundingClientRect().height);
11    }
12  });
13
14  return (
15    <div>
16      <div ref={divRef} style={{ height: '100px' }}>
17        This is a div.
18      </div>
19      <p>Div height: {height}px</p>
20    </div>
21  );
22};
23
24export default Example;

useEvent

有这样的一个 case

 1function Chat() {
 2  const [text, setText] = useState('');
 3
 4  const clickHandler = () => {
 5    sendMessage(text);
 6  };
 7
 8  return <SendButton onClick={clickHandler} />;
 9}

一个很简单的事件处理程序,点击 button 展示一下当前的 state,但 clickHandler 有一个问题只是透传 state,每次组件渲染都需要创新创建,这不合理,如果使用 useCallback 优化发现其实是个死循环

 1function Chat() {
 2  const [text, setText] = useState('');
 3
 4  const clickHandler = useCallback(() => {
 5    sendMessage(text);
 6  }, [text]);
 7
 8  return <SendButton onClick={clickHandler} />;
 9}

这时候其实可以使用 useRef 和 useLayoutEffect 解决这个问题

 1function useEvent(handler) {
 2  const handlerRef = useRef(null);
 3
 4  useLayoutEffect(() => {
 5    handlerRef.current = handler;
 6  });
 7
 8  return useCallback((...args) => {
 9    const fn = handlerRef.current;
10    return fn(...args);
11  }, []); 
12}

无论 state 是否发生变化,返回值总是稳定的,但每次组件重新渲染,会调用最新的 handlerRef.current,而 handlerRef.current 已经在每次渲染时更新为最新的 handler,handler 已经从闭包中获取到了最新的 state

 1function Chat() {
 2  const [text, setText] = useState('');
 3
 4  const clickHandler = useEvent(() => {
 5    sendMessage(text);
 6  });
 7
 8  return <SendButton onClick={clickHandler} />;
 9}

React 官方已经对 useEvent 有了 RFC github.com/reactjs/rfc…

你也许不需要 Effect

Effect 通常用于暂时跳出 React 代码并与外部系统进行同步,通过 useEffect 可以将包含副作用的逻辑(如数据获取、订阅等)从纯渲染逻辑中分离出来,使组件更加简洁和易维护,但如果组件中没有副作用,大可不必使用 Effect,React 官网教程也总结了无需使用 React 的几种 case,相信读了之后可以更好的使用 useEffect,列举几个印象深刻的

不要把可计算出来的逻辑写到 useEffect

当渲染使用的数据可以通过 props 和 state 计算得出时候,这个计算逻辑可以写在组件外层,不需要通过 useEffect 修改数据实现,典型例子就是 List 筛选

List 组件接收一个 items 列表作为 prop,然后用 state 变量 selection 来保持已选中的项。当 items 接收到一个不同的数组时,需要把 selection 重置为 null

 1function List({ items }) {
 2  const [selection, setSelection] = useState(null);
 3
 4  
 5  useEffect(() => {
 6    setSelection(null);
 7  }, [items]);
 8  
 9}

每当 items 变化时,List 及其子组件会先使用旧的 selection 值渲染,然后 React 会更新 DOM 并执行 Effect,最后调用 setSelection(null) 将导致 List 及其子组件重新渲染,重新启动整个流程。这样的写法本质上是把可计算的结果冗余的设置了 state,其实 selection item 完全可以直接依赖计算结果

 1function List({ items }) {
 2  
 3  const [selectedId, setSelectedId] = useState(null);
 4  
 5  const selection = items.find(item => item.id === selectedId) ?? null;
 6  
 7}

区分事件与 Effect

两者在很多时候会互相配合的对 state 进行修改,但其触发逻辑是不同的

  • 事件只在处理用户交互操作带来的变化时候运行
  • 每当依赖数据发生变化,useEffect 就会运行

当不确定某些代码是应该在 Effect 中还是在事件处理程序中时,可以反问自己为什么这段代码需要运行,Effect 只用来执行显示给用户时组件有必要执行的代码

比如有个 Form 组件需要发送两个请求:

  1. 在页面加载之际会发送一个分析请求
  2. 当用户填写表格并点击提交按钮时,它会向 /api/register 接口发送一个请求
 1function Form() {
 2  const [firstName, setFirstName] = useState('');
 3  const [lastName, setLastName] = useState('');
 4
 5  useEffect(() => {
 6    post('/analytics/event', { eventName: 'visit_form' });
 7  }, []);
 8
 9  const [jsonToSubmit, setJsonToSubmit] = useState(null);
10  useEffect(() => {
11    if (jsonToSubmit !== null) {
12      post('/api/register', jsonToSubmit);
13    }
14  }, [jsonToSubmit]);
15
16  
17  function handleSubmit(e) {
18    e.preventDefault();
19    setJsonToSubmit({ firstName, lastName });
20  }
21  
22}

第一个 useEffect 是合理的,因为表单显示给用户的时候就需要发型分析请求,但第二个明显不合理,这个请求不是渲染表单引起的,而是用户交互操作引起的,应该使用事件处理程序

 1function Form() {
 2  const [firstName, setFirstName] = useState('');
 3  const [lastName, setLastName] = useState('');
 4
 5  useEffect(() => {
 6    post('/analytics/event', { eventName: 'visit_form' });
 7  }, []);
 8
 9  function handleSubmit(e) {
10    e.preventDefault();
11    post('/api/register', { firstName, lastName });
12  }
13  
14}
个人笔记记录 2021 ~ 2025