移除 Effect 依赖项

编写 Effect 时,代码检查器会验证你是否已在 Effect 的依赖项列表中包含 Effect 读取的每个响应式值(如 props 和 state)。这确保你的 Effect 与组件的最新 props 和 state 保持同步。不必要的依赖项可能会导致你的 Effect 运行过于频繁,甚至创建无限循环。请遵循本指南来检查和移除 Effect 中不必要的依赖项。

你将学习

  • 如何修复无限 Effect 依赖循环
  • 当你想移除依赖项时该怎么做
  • 如何在不“响应”它的情况下从 Effect 中读取值
  • 如何以及为什么避免对象和函数依赖项
  • 为什么抑制依赖项代码检查器是危险的,以及该怎么做

依赖项应与代码匹配

编写 Effect 时,首先指定如何启动和停止你希望 Effect 执行的操作

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

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

然后,如果将 Effect 依赖项留空([]),代码检查器将建议正确的依赖项

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Fix the mistake here!
  return <h1>Welcome to the {roomId} room!</h1>;
}

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} />
    </>
  );
}

根据代码检查器的提示填写它们

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

Effects “响应”响应式值。 由于roomId 是一个响应式值(它可能会因重新渲染而改变),代码检查器会验证你是否已将其指定为依赖项。如果roomId 收到不同的值,React 将重新同步你的 Effect。这确保聊天保持连接到所选房间并“响应”下拉菜单

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  return <h1>Welcome to the {roomId} room!</h1>;
}

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 的依赖项。Effect 代码使用的每个响应式值都必须在你的依赖项列表中声明。依赖项列表由周围的代码确定

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

function ChatRoom({ roomId }) { // This is a reactive value
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
// ...
}

响应式值包括 props 和在组件内部直接声明的所有变量和函数。由于roomId 是一个响应式值,你无法将其从依赖项列表中移除。代码检查器不允许这样做

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

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

代码检查器是对的!由于roomId 可能会随着时间而改变,这会在你的代码中引入错误。

要移除依赖项,请向代码检查器“证明”它不需要成为依赖项。 例如,你可以将roomId 从组件中移出,以证明它不是响应式的,并且不会在重新渲染时发生变化

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

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

现在roomId 不是响应式值(并且不会在重新渲染时发生变化),它不需要成为依赖项

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

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the {roomId} room!</h1>;
}

这就是为什么你现在可以指定一个空的([])依赖项列表。 你的 Effect实际上并不依赖于任何响应式值,因此它实际上并不需要在组件的任何 props 或 state 发生更改时重新运行。

要更改依赖项,请更改代码

你可能已经注意到你的工作流程中有一种模式

  1. 首先,你更改 Effect 的代码或响应式值的声明方式。
  2. 然后,你遵循代码检查器的建议并调整依赖项以匹配你已更改的代码。
  3. 如果你对依赖项列表不满意,则返回第一步(并再次更改代码)。

最后部分很重要。如果要更改依赖项,请先更改周围的代码。您可以将依赖项列表视为Effect 代码中使用的所有反应式值的列表。 您无需选择列表中要包含的内容。该列表描述您的代码。要更改依赖项列表,请更改代码。

这可能感觉像是在解方程。您可能从一个目标开始(例如,删除一个依赖项),并且需要“找到”与该目标匹配的代码。并非每个人都觉得解方程很有趣,编写 Effect 也一样!幸运的是,下面列出了一些您可以尝试的常用方法。

陷阱

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

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

当依赖项与代码不匹配时,存在引入错误的极高风险。通过抑制 linter,您向 React “撒谎”了您的 Effect 依赖的值。

请改用以下技术。

深入探讨

为什么抑制依赖项 linter 如此危险?

抑制 linter 会导致非常不直观的错误,难以查找和修复。以下是一个示例

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

假设您只想在“挂载时”运行 Effect。您已阅读空 ([]) 依赖项 可以做到这一点,因此您决定忽略 linter,并强制指定[]作为依赖项。

此计数器应该每秒增加可通过两个按钮配置的数量。但是,由于您向 React “撒谎”说此 Effect 不依赖任何内容,因此 React 将永远使用初始渲染中的onTick函数。在该渲染期间,count0increment1。这就是为什么该渲染中的onTick总是每秒调用setCount(0 + 1),您总是看到1的原因。当此类错误分散在多个组件中时,更难修复。

总有比忽略 linter 更好的解决方案!要修复此代码,您需要将onTick添加到依赖项列表中。(为了确保仅设置一次间隔,使onTick成为 Effect 事件。)

我们建议将依赖项 lint 错误视为编译错误。如果您不抑制它,您将永远不会看到这样的错误。此页面的其余部分介绍了针对这种情况和其他情况的替代方法。

删除不必要的依赖项

每次调整 Effect 的依赖项以反映代码时,请查看依赖项列表。当这些依赖项中的任何一个发生更改时,Effect 是否重新运行是有意义的吗?有时,答案是“否”。

  • 您可能希望在不同的条件下重新执行 Effect 的不同部分
  • 您可能只想读取某些依赖项的最新值,而不是“对”其更改做出反应。
  • 由于依赖项是对象或函数,因此它可能会无意中过于频繁地更改。

要找到正确的解决方案,您需要回答一些关于 Effect 的问题。让我们一起学习。

此代码是否应该移至事件处理程序?

首先应该考虑的是这段代码是否应该是一个 Effect。

想象一下一个表单。提交时,您将submitted状态变量设置为true。您需要发送 POST 请求并显示通知。您已将此逻辑放入一个“对”submittedtrue做出反应的 Effect 中。

function Form() {
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

稍后,您想根据当前主题设置通知消息的样式,因此您读取当前主题。由于theme是在组件主体中声明的,它是一个反应式值,因此您将其添加为依赖项。

function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ All dependencies declared

function handleSubmit() {
setSubmitted(true);
}

// ...
}

通过这样做,您引入了一个错误。假设您先提交表单,然后在深色和浅色主题之间切换。theme将更改,Effect 将重新运行,因此它将再次显示相同的通知!

这里的问题是,这根本不应该是一个 Effect。您想在提交表单时发送此 POST 请求并显示通知,这是一种特殊的交互。要根据特定交互运行一些代码,请将该逻辑直接放入相应的事件处理程序中。

function Form() {
const theme = useContext(ThemeContext);

function handleSubmit() {
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!', theme);
}

// ...
}

现在代码位于事件处理程序中,它不是反应式的——因此它只会在用户提交表单时运行。阅读更多关于在事件处理程序和 Effect 之间进行选择以及如何删除不必要的 Effect。

您的 Effect 是否正在执行几件无关的事情?

您应该问自己的下一个问题是您的 Effect 是否正在执行几件无关的事情。

想象一下,您正在创建一个发货表单,用户需要选择他们的城市和地区。您根据所选的国家从服务器获取城市列表,并在下拉菜单中显示。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

// ...

这是一个在Effect中获取数据的好例子。您根据国家属性将城市状态与网络同步。您不能在事件处理程序中执行此操作,因为您需要在ShippingForm显示时以及国家发生更改时(无论哪个交互导致更改)立即获取数据。

现在假设您正在添加第二个用于城市地区的选框,它应该为当前选择的城市获取地区列表。您可能首先尝试在同一个Effect中添加第二个fetch调用来获取地区列表。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared

// ...

但是,由于Effect现在使用城市状态变量,您必须将城市添加到依赖项列表中。这反过来又引入了一个问题:当用户选择不同的城市时,Effect将重新运行并调用fetchCities(country)。结果,您将不必要地多次重新获取城市列表。

这段代码的问题在于您正在同步两个不同的、不相关的事情。

  1. 您希望根据国家属性将城市状态与网络同步。
  2. 您希望根据城市状态将地区状态与网络同步。

将逻辑分成两个Effect,每个Effect都响应它需要与其同步的属性。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared

// ...

现在,第一个Effect只有在国家发生更改时才会重新运行,而第二个Effect在城市发生更改时重新运行。您已按目的将它们分开:两个不同的Effect同步两个不同的东西。两个独立的Effect具有两个独立的依赖项列表,因此它们不会无意中触发彼此。

最终代码比原始代码长,但拆分这些Effect仍然是正确的。每个Effect都应代表一个独立的同步过程。在这个例子中,删除一个Effect不会破坏另一个Effect的逻辑。这意味着它们同步不同的东西,最好将它们分开。如果您担心重复,您可以通过将重复的逻辑提取到自定义Hook中来改进此代码。

您是否正在读取某些状态来计算下一个状态?

此Effect每次收到新消息时都会使用新创建的数组更新messages状态变量。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...

它使用messages变量来创建一个新数组,该数组以所有现有消息开头,并在末尾添加新消息。但是,由于messages是Effect读取的反应式值,因此它必须是一个依赖项。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...

messages设为依赖项会引入一个问题。

每次收到消息时,setMessages()都会导致组件使用包含已接收消息的新messages数组重新渲染。但是,由于此Effect现在依赖于messages,这也会重新同步Effect。因此,每条新消息都会使聊天重新连接。用户不会喜欢这样!

要解决此问题,请不要在Effect内部读取messages。而是向setMessages传递一个更新函数

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

注意,您的Effect现在根本没有读取messages变量。您只需要传递一个更新函数,例如msgs => [...msgs, receivedMessage]。React将您的更新函数放入队列中,并在下次渲染期间向其提供msgs参数。这就是为什么Effect本身不再需要依赖于messages的原因。由于此修复,接收聊天消息将不再使聊天重新连接。

您是否想读取某个值而不“响应”其更改?

建设中

本节描述的是尚未在React稳定版本中发布的实验性API

假设您希望在用户收到新消息时播放声音,除非isMutedtrue

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...

由于您的Effect现在在其代码中使用isMuted,因此您必须将其添加到依赖项中。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ All dependencies declared
// ...

问题是,每次isMuted发生更改(例如,当用户按下“静音”切换按钮时),Effect都会重新同步并重新连接到聊天。这不是理想的用户体验!(在此示例中,即使禁用linter也不起作用——如果您这样做,isMuted将“卡住”其旧值。)

要解决此问题,您需要将不应该具有反应性的逻辑从Effect中提取出来。您不希望此Effect“响应”isMuted中的更改。将此非反应性逻辑移动到Effect事件中:

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

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effect事件允许您将Effect拆分为反应性部分(应该“响应”反应式值,例如roomId及其更改)和非反应性部分(仅读取其最新值,例如onMessage读取isMuted)。现在您在Effect事件中读取isMuted,它不需要成为Effect的依赖项。结果,当您打开和关闭“静音”设置时,聊天不会重新连接,从而解决了原始问题!

从 props 中包装事件处理程序

当您的组件将事件处理程序作为 prop 接收时,您可能会遇到类似的问题。

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ All dependencies declared
// ...

假设父组件在每次渲染时都传递一个不同的 onReceiveMessage 函数。

<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>

由于 onReceiveMessage 是一个依赖项,因此它会在每次父组件重新渲染后导致 Effect 重新同步。这将使其重新连接到聊天。为了解决这个问题,请将调用包装在 Effect 事件中。

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effect 事件不是反应式的,因此您不需要将其指定为依赖项。结果,即使父组件传递的函数在每次重新渲染时都不同,聊天也不会重新连接。

分离反应式和非反应式代码

在这个示例中,您希望每次 roomId 更改时记录一次访问。您希望在每次日志中包含当前的 notificationCount,但您希望 notificationCount 的更改触发日志事件。

解决方案再次是将非反应式代码拆分为 Effect 事件。

function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});

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

您希望您的逻辑对 roomId 具有反应性,因此您在 Effect 内读取 roomId。但是,您不希望 notificationCount 的更改记录额外的访问,因此您在 Effect 事件中读取 notificationCount了解如何使用 Effect 事件从 Effects 读取最新的 props 和状态。

某些反应式值是否无意中更改?

有时,您确实希望您的 Effect “对”某个值做出反应,但该值的更改频率高于您想要的——并且可能无法反映用户视角的任何实际更改。例如,假设您在组件的主体中创建一个 options 对象,然后从 Effect 内部读取该对象。

function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...

此对象在组件主体中声明,因此它是一个反应式值。当您在 Effect 内部这样读取反应式值时,您将其声明为依赖项。这确保您的 Effect “对”其更改做出反应。

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

将其声明为依赖项非常重要!例如,这确保如果 roomId 更改,您的 Effect 将使用新的 options 重新连接到聊天。但是,上面的代码也存在问题。要查看它,请尝试在下面的沙盒中键入输入,并观察控制台中发生的情况。

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('');

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

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

  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} />
    </>
  );
}

在上面的沙盒中,输入仅更新 message 状态变量。从用户的角度来看,这不会影响聊天连接。但是,每次更新 message 时,您的组件都会重新渲染。当您的组件重新渲染时,其中的代码将从头开始重新运行。

在每次 ChatRoom 组件重新渲染时,都会从头开始创建一个新的 options 对象。React 发现 options 对象与上次渲染期间创建的 options 对象是不同的对象。这就是为什么它会重新同步您的 Effect(它依赖于 options),以及您在键入时聊天重新连接的原因。

此问题仅影响对象和函数。在 JavaScript 中,每个新创建的对象和函数都被认为与所有其他对象和函数不同。它们内部的内容是否相同并不重要!

// During the first render
const options1 = { serverUrl: 'https://127.0.0.1:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://127.0.0.1:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

对象和函数依赖项可能会使您的 Effect 比您需要的更频繁地重新同步。

因此,应尽可能避免将对象和函数作为 Effect 的依赖项。相反,尝试将它们移到组件外部、Effect 内部,或从中提取基本值。

将静态对象和函数移到组件外部

如果对象不依赖于任何 props 和状态,您可以将该对象移到组件外部。

const options = {
serverUrl: 'https://127.0.0.1:1234',
roomId: 'music'
};

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

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

这样,您可以向 linter 证明它不是反应式的。它不会因重新渲染而更改,因此不需要成为依赖项。现在重新渲染 ChatRoom 不会导致您的 Effect 重新同步。

这对函数也有效。

function createOptions() {
return {
serverUrl: 'https://127.0.0.1:1234',
roomId: 'music'
};
}

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

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...

由于 createOptions 在组件外部声明,因此它不是反应式值。这就是为什么它不需要在 Effect 的依赖项中指定,以及为什么它永远不会导致您的 Effect 重新同步的原因。

将动态对象和函数移到 Effect 内部

如果您的对象依赖于某些反应式值,这些值可能会因重新渲染而更改,例如 roomId prop,则您无法将其移到组件外部。但是,您可以将其创建移到Effect 代码内部

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]); // ✅ All dependencies declared
// ...

现在,由于 options 声明在 Effect 内部,它不再是 Effect 的依赖项。取而代之的是,您的 Effect 使用的唯一反应式值是 roomId。由于 roomId 不是对象或函数,您可以确定它不会意外地发生变化。在 JavaScript 中,数字和字符串是按内容进行比较的。

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

由于这个修复,如果您编辑输入内容,聊天将不再重新连接。

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} />
    </>
  );
}

但是,正如您预期的那样,当您更改 roomId 下拉菜单时,它重新连接。

这对函数也有效。

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]); // ✅ All dependencies declared
// ...

您可以编写自己的函数来将逻辑片段分组到 Effect 内部。只要您也 Effect 内部声明它们,它们就不是反应式值,因此不需要成为 Effect 的依赖项。

从对象中读取原始值

有时,您可能会从 props 中收到一个对象。

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

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

这里的风险是父组件会在渲染过程中创建该对象。

<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>

这会导致您的 Effect 在父组件每次重新渲染时都重新连接。为了解决这个问题,请在 Effect *外部*读取对象中的信息,并避免对象和函数依赖性。

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

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

逻辑变得有点重复(您从 Effect 外部的对象中读取一些值,然后在 Effect 内部创建一个具有相同值的相同对象)。但这明确地说明了您的 Effect *实际*依赖哪些信息。如果父组件意外地重新创建了一个对象,聊天将不会重新连接。但是,如果 options.roomIdoptions.serverUrl 确实不同,则聊天会重新连接。

从函数计算原始值

同样的方法也适用于函数。例如,假设父组件传递一个函数。

<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>

为了避免使其成为依赖项(并导致其在重新渲染时重新连接),请在 Effect 外部调用它。这会为您提供不是对象的 roomIdserverUrl 值,您可以从 Effect 内部读取这些值。

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

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

这只适用于 函数,因为它们可以在渲染期间安全地调用。如果您的函数是一个事件处理程序,但您不希望它的更改重新同步您的 Effect,请将其包装到 Effect 事件中。

回顾

  • 依赖项应始终与代码匹配。
  • 当您对依赖项不满意时,您需要编辑的是代码。
  • 抑制代码检查器会导致非常令人困惑的错误,您应该始终避免这样做。
  • 要删除依赖项,您需要向代码检查器“证明”它是不必要的。
  • 如果某些代码应该响应特定的交互而运行,请将该代码移动到事件处理程序。
  • 如果 Effect 的不同部分应该由于不同的原因而重新运行,请将其拆分为多个 Effect。
  • 如果要根据先前状态更新某些状态,请传递一个更新程序函数。
  • 如果要读取最新值而不“响应”它,请从 Effect 中提取一个 Effect 事件。
  • 在 JavaScript 中,如果对象和函数是在不同时间创建的,则它们被认为是不同的。
  • 尽量避免对象和函数依赖性。将它们移动到组件外部或 Effect 内部。

挑战 1 4:
修复重置间隔

此 Effect 设置一个每秒钟运行一次的间隔。您注意到发生了一些奇怪的事情:每次间隔运行时,它似乎都会被销毁并重新创建。修复代码,以便间隔不会不断地重新创建。

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Counter: {count}</h1>
}