useEffect 是一个 React Hook,它使你可以 将组件与外部系统同步。

useEffect(setup, dependencies?)

参考

useEffect(setup, dependencies?)

在组件的顶级调用 useEffect 以声明一个 Effect

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

请参阅以下更多示例。

参数

  • setup:带有效果逻辑的函数。setup 函数还可以选择返回一个清理函数。当组件添加到 DOM 时,React 将运行 setup 函数。每次在更改依赖项后重新渲染时,React 会先使用旧值运行清理函数(如果已提供),然后使用新值运行 setup 函数。当组件从 DOM 中删除后,React 将运行清理函数。

  • 可选 dependencies:在 setup 代码中引用所有响应值列表。响应值包括道具、状态以及直接在组件体中声明的所有变量和函数。如果代码检查程序已 为 React 配置,它将验证每个响应值是否正确指定为依赖项。依赖项列表必须包含固定数量的项并按 [dep1, dep2, dep3] 的形式内联编写。React 将使用 Object.is 比较将每个依赖项与其上一个值进行比较。如果你省略此参数,你的 Effect 将在组件每次重新渲染后重新运行。 请查看传递依赖项数组、空数组以及不传递任何依赖项之间的差异。

返回

useEffect 返回 undefined

注意事项

  • useEffect 为 Hook,因此您只能在 **组件的最顶层** 或您自己的 Hooks 中调用它。您不能在循环或条件内部调用它。如果您需要此功能,请提取一个新组件并将其中的 state 移入其中。

  • 如果您不是 **试图与某个外部系统同步**,您可能无需 Effect。

  • 开启严格模式后,React 会在首次实际设置之前 **运行一个额外的仅开发设置+清除周期**。这是一次压力测试,用以确保清除逻辑“镜像”设置逻辑并且它停止或撤消设置所执行的操作。如果这引起了问题,请 实现清除函数。

  • 如果某些依赖项是在组件内定义的对象或函数,则它们可能会 导致效果比需要的情况重跑得更频繁。要修复此问题,请删除不必要的 对象函数 依赖项。您还可以 提取状态更新非响应式逻辑,放在效果之外。

  • 如果效果不是由交互(如点击)引起的,React 通常会让浏览器 先绘制更新的屏幕,然后再运行您的效果。如果你的效果正在执行一些视觉上的东西(例如,定位提示框),并且延迟很明显(例如,闪烁),用 useLayoutEffect 替换 useEffect

  • 如果你的效果是由交互行为引起的(例如点击),React 会在浏览器绘制更新的屏幕之前运行你的效果。这将确保事件系统可以观察到效果的结果。通常,这种情况会按预期工作。但是,如果你必须将这项工作推迟到绘制之后,比如 alert(),你可以使用 setTimeout。请参阅 reactwg/react-18/128 以获取更多信息。

  • 即使你的效果是由交互行为引起的(例如点击),React 会允许浏览器在你效果里的状态更新处理之前重新绘制屏幕。通常,这种情况会按预期工作。但是,如果你必须阻止浏览器重新绘制屏幕,你需要用 useLayoutEffect 替换 useEffect

  • 效果 仅在客户端上运行。它们不会在服务器渲染期间运行。


使用

连接到外部系统

当一些组件在页面上展示时,它们需要与网络、浏览器 API 或第三方库保持连接。这些系统不受 React 控制,因此称为外部系统。

要将您的组件连接到某些外部系统,请在组件的顶层调用 useEffect

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

您需要将两个参数传递给 useEffect

  1. 设置代码设置函数,用于连接到该系统。
    • 它应该返回带 清理代码清理函数,用于断开与该系统的连接。
  2. 一个 依赖项列表,包括这些函数中所用组件中的每个值。

React 会在每次必要时调用您的设置函数和清理函数,可能多次调用

  1. 當您的元件加入到頁面時,您的設定程式碼會執行(掛載)
  2. 在您的元件每次重複繪製,而相依元件已經改變時
    • 首先,您的清除程式碼會使用舊的屬性和狀態執行。
    • 然後,您的設定程式碼會使用新的屬性和狀態執行。
  3. 在您的元件從頁面移除後,您的清除程式碼會最後執行一次(解除掛載)

讓我們為上述範例說明此順序。

當上方的ChatRoom元件加入頁面時,它會連線到聊天室,並使用初始serverUrl以及roomId。如果serverUrlroomId因為重新繪製而有改變(例如:使用者在下拉式選單中挑選另一個聊天室),您的效果會與前一個房間斷線,並連線至下一個房間。當ChatRoom元件從頁面上移除時,您的效果會最後斷線一次。

为了帮助你发现 bug,在开发环境中,React 在setup之前多运行一次cleanup这是一个压力测试,用于验证你的 Effect 的逻辑是否正确实现。如果这导致明显的问题,则你的 cleanup 函数缺少一些逻辑。cleanup 函数应该停止或撤消 setup 函数所做的任何事情。经验法则是,用户不应该能够区分作为一次调用的 setup(如在生产环境中)和setupcleanupsetup 序列(如在开发环境中)。参见通用解决方案。

尝试将每个 Effect 写为一个独立的进程一次考虑一个 setup/cleanup 周期。你的组件是装入、更新还是卸载都不必是重要的。当你的 cleanup 逻辑正确地“镜像”了 setup 逻辑时,你的 Effect 能够根据需要频繁地运行 setup 和 cleanup。

注意

Effect 让你能够让你的组件与外部系统(如聊天服务)保持同步。此处,外部系统是指不受 React 控制的任何代码片段,例如

如果不连接到任何外部系统,你可能不需要 Effect。

连接外部系统的示例

示例 1of 5:
连接到聊天服务器

在此示例中,ChatRoom 组件使用一种效果保持与 chat.js 中定义的外部系统连接。点击“打开聊天”以使 ChatRoom 组件显示。此沙盒以开发模式运行,因此有一个额外的连接和断开连接周期,正如在此处所解释。尝试使用下拉列表和输入更改 roomIdserverUrl,看看该效果如何重新连接到聊天。点击“关闭聊天”以查看该效果最后一次断开连接。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}


将效果封装在自定义 Hook 中

Effects 是一种“逃生舱”:当你需要“跳出 React”时使用它们,并且对于你的用例不存在更好的内置解决方案。如果你发现自己经常需要手动编写 Effects,则通常表明你需要提取一些自定义 Hooks,以便组件依赖于你的常见行为。

例如,此useChatRoom自定义 Hook 在更具声明性的 API 后面“隐藏”了你的 Effect 的逻辑

function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

然后,你可以这样从任何组件使用它

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

在 React 生态系统中还有很多针对每种用途的出色的自定义 Hooks。

了解更多有关在自定义 Hooks 中包装 Effects 的信息。

在自定义 Hooks 中包装 Effects 的示例

示例 1of 3:
自定义 useChatRoom Hook

此示例与 之前的某个示例 相同,但逻辑已提取到自定义 Hook。

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}


控制非 React 小组件

有时,您希望将外部系统与组件的某个属性或状态保持同步。

比如,如果你有一个无 React 编写的第三方地图小部件或视频播放器组件,你可以使用一个效果来调用其上的方法,使它的状态匹配你 React 组件的当前状态。此效果创建一个 MapWidget 类的实例,该类定义在 map-widget.js 中。当你更改 Map 组件的 zoomLevel prop 时,该效果会调用该类实例上的 setZoom(),以保持其同步

import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}

在这个示例中,不需要清理函数,因为 MapWidget 类仅管理传递给它的 DOM 节点。在从 tree 中移除 Map React 组件后,DOM 节点和 MapWidget 类实例都将自动由浏览器 JavaScript 引擎垃圾回收。


使用 Effects 获取数据

你可以使用 Effect 为你的组件获取数据。请注意如果你使用了一个框架,使用框架的数据获取机制会比手动编写 Effects 的效率要高得多。

如果你想手动从 Effect 获取数据,你的代码可能看上去是这样

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);

useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);

// ...

注意ignore变量,它被初始化为false,并且在清理时被设置为true。这样可以确保你的代码不会受到"竞争条件"的影响:网络响应可能会按与你发送它们的不同的顺序到达。

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

你也可以使用async / await语法重写,但你仍然需要提供一个清理函数

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

直接在 Effects 中编写数据获取会变得重复,并使得以后难以添加诸如缓存和服务器渲染之类的优化。使用自定义 Hook 更容易,它可以是你自己的,也可以由社区维护。

深入了解

Effect 中获取数据的理想替代方案是什么?

Effect 中编写获取调用是一种获取数据的一种流行方法,这种方法在完全客户端应用程序中尤其流行。然而,这是一个非常手动的做法,并且有很多缺点

  • Effect 在服务器上不会运行。这意味着服务器最初呈现的 HTML 只包含一个加载状态,不包含数据。客户端计算机将不得不下载所有 JavaScript 并呈现你的应用程序,最后却发现它现在需要加载数据。这种方式的效率非常低。
  • 在 Effect 中直接获取容易形成“网络瀑布”。呈现父组件,获取一些数据,呈现子组件,然后子组件开始获取它们的数据。如果网络不是非常快,这会比并行获取所有数据慢得多。
  • 在 Effects 中直接获取通常意味着你不预加载或缓存数据。例如,如果组件卸载然后再次加载,它会再次获取数据。
  • 这并非十分便利。在编写 fetch 调用时会涉及相当多的样板代码,而这种代码并不会出现诸如 竞争条件 等问题。

此缺点列表并非 React 所特有。它适用于使用任何库在 mount 时获取数据的情况。与路由一样,数据获取并非易事,因此我们推荐以下方法

  • 如果您使用 框架,请使用其内置的数据获取机制。现代 React 框架集成了高效且不会出现上述缺点的数据获取机制。
  • 否则,请考虑使用或构建客户端缓存。流行的开源解决方案包括 React QueryuseSWRReact Router 6.4+。您还可以构建自己的解决方案,在这种情况下,您将在基础中使用 Effects,但还要添加用于消除重复请求、缓存响应及避免网络瀑布的逻辑(通过预加载数据或将数据要求提升至路由)。

如果您不适合这些方法,则可以继续在 Effects 中直接获取数据。


指定 reactive 依赖

注意,你无法 "选择" Effect 的依赖项。Effect 的代码使用的每个 reactive 值 都必须声明为一个依赖项。Effect 的依赖项列表由周围代码决定

function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); // This is a reactive value too

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
}

如果 serverUrlroomId 发生更改,你的 Effect 就会使用新值重新连接到聊天

Reactive 值 包括 prop 以及在你组件内直接声明的所有变量和函数。由于 roomIdserverUrl 是 reactive 值,你无法将它们从依赖项中移除。如果你尝试忽略它们,并且 已为 React 正确配置了 linter, linter 将将其标记为你需要修复的错误

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
}

要删除依赖项,你需要“向 linter 证明”该依赖项不需要存在。例如,你可以将serverUrl 从你的组件移出以证明它没有反应性,并且不会因重新渲染而改变

const serverUrl = 'https://127.0.0.1:1234'; // Not a reactive value anymore

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}

serverUrl 不再是反应值(并且无法在重新渲染时改变)时,它无需成为一个依赖项。如果你的 Effect 的代码不使用任何反应值,它的依赖项列表应该为空([]):

const serverUrl = 'https://127.0.0.1:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
}

具有空依赖项的 Effect在你的组件的任何 props 或状态更改时都不会重新运行。

陷阱

如果你有一个现有的代码库,你可能有一些像这样抑制 linter 的 Effect

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

当依赖项不匹配代码时,很有可能引入 bug。通过抑制 linter,你向 React “撒谎”,声称你的 Effect 依赖于这些值。相反,证明它们是不必要的。

传递响应性依赖关系的示例

示例 1of 3:
传递依赖关系数组

如果你指定依赖项,则你的 Effect 在初始渲染后以及在依赖项发生更改后重新渲染后运行。

useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different

在下面的示例中,serverUrlroomId响应性值,因此必须将它们都指定为依赖项。因此,在下拉列表中选择不同的房间或编辑服务器 URL 输入会使聊天重新连接。但是,由于message 未在 Effect 中使用(因此它不是依赖项),因此编辑消息不会重新连接到聊天。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
      <label>
        Your message:{' '}
        <input value={message} onChange={e => setMessage(e.target.value)} />
      </label>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
        <button onClick={() => setShow(!show)}>
          {show ? 'Close chat' : 'Open chat'}
        </button>
      </label>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId}/>}
    </>
  );
}


基于来自效果的前一状态更新状态

当你想从效果中基于前一状态更新状态时,你可能遇到一个问题

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

useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
}

因为 count 是一个响应的值,必须在依赖项列表中指定。然而,造成了效果每次 count 改变时都会清理并重新设置。这不是理想的。

要修复此问题,c => c + 1 状态更新传递setCount

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Pass a state updater
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Now count is not a dependency

  return <h1>{count}</h1>;
}

现在,你传递 c => c + 1 而不是 count + 1你的 Effect 不再依赖于 count由于此修复,它无需每次 count 更改时再次清理和设置时间间隔。


正在移除无必要的对象依赖项

如果 Effect 依赖于渲染期间创建的对象或函数,它可能会运行得太过频繁。例如,该 Effect 每次渲染后都会重新连接,因为 options 对象在每次渲染时都不同:

const serverUrl = 'https://127.0.0.1:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ...

避免在渲染期间使用创建的对象作为依赖项。相反,在 Effect 中创建对象

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://127.0.0.1:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

现在,你可以在 Effect 中创建 options 对象,Effect 本身只依赖于 roomId 字符串。

通过此修复,键入输入不会重新连接聊天。与重新创建的对象不同,除非您将其设置为另一个值,否则 roomId 这样的字符串不会更改。阅读有关如何删除依赖项的更多信息。


删除不必要的函数依赖项

如果您的 Effect 取决于渲染期间创建的对象或函数,则它可能运行得太频繁。例如,此 Effect 在每次渲染后都会重新连接,因为 createOptions 函数对于每次渲染来说都是不同的:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}

useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
// ...

本身而言,每次重新渲染时从头开始创建函数并不是问题。您不需要对此进行优化。但是,如果您将其用作 Effect 的依赖项,它将导致您的 Effect 在每次重新渲染后重新运行。

避免将渲染期间创建的函数用作依赖项。相反,在 Effect 内声明它

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://127.0.0.1:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

现在,已在 Effect 内部就 createOptions 函数进行了定义,因此 Effect 本身仅依赖于 roomId 字符串。通过此修复,聊天框不会在输入期间重新连接。与重新创建的函数不同,除非将 roomId 等字符串设置为另一个值,否则它不会发生更改。详细了解如何解除依赖性。


从 Effect 中读取最新 props 和状态

正在建设中

本部分描述了 尚未在 React 稳定版本中发布的实验性 API

默认情况下,从 Effect 中读取反应式值时,必须将该值添加为依赖项。这可确保 Effect “针对”该值的每次更改做出“反应”。对于大多数依赖项,这就是所需的行为。

然而,有时可能希望从 Effect 中读取最新的 props 和状态,而无需对其做出“反应”。例如,设想希望为每次访问页面记录购物车的商品数量

function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}

如果您希望在每次 url 更改后记录一个新页面访问,但只有 shoppingCart 更改时除外,该怎么办? 您不能在不破坏 反应性规则 的情况下从依赖项中排除 shoppingCart。但是,您可以表示您不希望一段代码 “响应” 更改,即使它是在 Effect 中调用。声明一个 Effect Event(效应事件),使用 useEffectEvent Hook,然后将读取 shoppingCart 的代码移至其中

function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}

Effect Event(效应事件)没有反应性且必须始终从您的 Effect 的依赖项中省略。 这就是您可以将非反应性代码(您可以从中读取某些属性和状态的最新值)放置在其中。onVisit 中通过读取 shoppingCart,您可以确保 shoppingCart 不会重新运行您的 Effect。

详细了解 Effect Event(效应事件)如何使您能够分离反应性和非反应性代码。


在服务器和客户端上显示不同内容

如果你的应用使用了服务端渲染(直接使用 直接 使用或使用 框架),你的组件将会在两个不同的环境中渲染。在服务端,它将会渲染生成初始 HTML。在客户端,React 将再次执行渲染代码,以便将事件处理程序附加到该 HTML。这就是为什么对于 水合 发挥作用,你的初始渲染输出必须在客户端和服务端保持相同。

在稀有情况下,你可能需要在客户端上显示不同内容。例如,如果你的应用从 localStorage 中读取了一些数据,那么它不可能在服务端上执行此操作。以下是如何实现此操作的

function MyComponent() {
const [didMount, setDidMount] = useState(false);

useEffect(() => {
setDidMount(true);
}, []);

if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}

在应用加载时,用户将看到初始渲染输出。然后,在加载并完成水合之后,你的 Effect 将运行并将 didMount 设置为 true,触发重新渲染。这将切换到仅限客户端的渲染输出。Effect 在服务端上不运行,所以这就是为什么在初始服务端渲染期间 didMountfalse 的原因。

谨慎使用此模式。请记住,网络连接速度较慢的用户会相当长的时间内(可能好几秒)看到初始内容,所以你不想对你的组件的外观进行大幅更改。在许多情况下,你可以通过使用 CSS 有条件地显示不同内容来避免出现此需求。


故障排除

组件加载时,我的效果运行了两次

在开发模式下,当启用严格模式时,React 会在实际设置之前额外运行一次设置和清理。

这是一个压力测试,用于验证您效果的逻辑是否已正确实现。如果这导致出现明显的问题,表明您的清理功能缺少一些逻辑。清理功能应停止或撤消设置功能执行的动作。经验法则是,用户不应该能够区分调用设置一次(如在生产中)和设置 → 清理 → 设置序列(如在开发中)

阅读更多与此帮助查找错误如何修复你的代码逻辑问题。有关的信息。


我的副作用在每次重新渲染之后都运行

首先,检查你是否忘记指定依赖项数组

useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!

如果你已指定依赖项数组,但副作用仍会循环重新运行,则可能是因为每次重新渲染时,某个依赖项不同。

你可以通过手动记录依赖项到控制台来调试此问题

useEffect(() => {
// ..
}, [serverUrl, roomId]);

console.log([serverUrl, roomId]);

然后,你可以右键单击控制台中不同重新渲染的数组,并为两者都选择“存储为全局变量”。假设第一个被保存为 temp1,第二个被保存为 temp2,然后,你可以使用浏览器控制台检查两个数组中的每个依赖项是否相同

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

当你找到每次重新渲染时不同的依赖项时,通常可以用以下方法之一进行修复

作为最后的手段(如果这些方法没有帮助的话),用 useMemouseCallback 包裹它的创建(对于函数而言)。


我的 Effect 在一个无限循环中持续运行

如果你的 Effect 在一个无限循环中运行,这两个情况一定是真实的

  • 你的 Effect 正在更新某些状态。
  • 该状态导致重新渲染,这会导致 Effect 的依赖项发生变化。

在你开始解决这个问题之前,问问你自己,你的 Effect 是否与某些外部系统(如 DOM、网络、第三方小部件等)相连接。你的 Effect 为什么需要设置状态?它是否与该外部系统同步?还是你试图用它来管理应用程序的数据流?

如果没有外部系统,考虑一下是否 完全删除 Effect 会简化你的逻辑。

如果您确实正在与某个外部系统同步,请思考一下应在何时以及在什么情况下让 Effect 来更新状态。会不会有些信息发生了变更而影响组件的视觉输出表现?如果您需要持续追踪某部分未使用通过渲染,则 ref(不会触发重新渲染)可能更合适。确认一下,您的 Effect 是否像必要时那样更新状态(并触发重新渲染)。

最后,如果您的 Effect 在正确的时间更新了状态,但仍有一个循环,这可能是因为状态更新导致 Effect 的某个依赖项发生了改变。阅读此处以了解如何调试依赖项变更。


我的清除逻辑运行,即使我的组件未卸载

清除函数不仅在卸载期间运行,而且在每次具有已更改依赖项的重新渲染期间运行。此外,在开发过程中,React 在组件安装后立即运行一次额外的安装+清除

如果您有清理由代码而没有相应的安装代码,通常会产生代码异味

useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);

您的清除逻辑应该与安装逻辑“对称”,并应停止或撤消所有安装操作

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);

了解 Effect 生命周期与组件生命周期的差异。


我的 Effect 负责视觉上的事情,但我在它运行之前看到了一段闪烁

如果你的 Effect 会阻止浏览器绘制屏幕, 请将 useEffect 替换为 useLayoutEffect。请注意,对于绝大多数的效果而言,这是没有必要的。只有在浏览器绘制前必须运行你的效果时,才需要这样做:例如,在用户看到工具提示前测量和定位工具提示。