移除 Effect 依赖项

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

你将学习

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

依赖项应该与代码匹配

当你编写 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 依赖项留空 ([]),linter 会建议正确的依赖项

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

根据 linter 的提示填写它们

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

Effect 会“响应”响应式值。 由于 roomId 是一个响应式值(它可能会因重新渲染而改变),因此 linter 会验证你是否已将其指定为依赖项。如果 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 是一个响应式值,因此你无法将其从依赖项列表中移除。linter 不允许这样做

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'
// ...
}

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

要移除依赖项,请向 linter“证明”它*不需要*成为依赖项。 例如,你可以将 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. 然后,您遵循 linter 并调整依赖项以匹配您已更改的代码。
  3. 如果您对依赖项列表不满意,则返回到第一步(并再次更改代码)。

最后一部分很重要。如果要更改依赖项,请先更改周围的代码。 您可以将依赖项列表视为Effect 代码使用的所有响应式值的列表。 您不会选择要放在该列表上的内容。 该列表描述了您的代码。 要更改依赖项列表,请更改代码。

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

陷阱

如果您有一个现有的代码库,则您可能有一些像这样抑制 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 函数。 在该渲染期间, count0 并且 increment1。 这就是为什么该渲染中的 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 和 state,则可以将该对象移到组件外部。

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 Event 中。

总结

  • 依赖项应始终与代码匹配。
  • 当您对依赖项不满意时,您需要编辑的是代码。
  • 禁止 linter 会导致非常令人困惑的错误,您应该始终避免这样做。
  • 要删除依赖项,您需要向 linter “证明”它不是必需的。
  • 如果某些代码应该响应特定的交互而运行,请将该代码移动到事件处理程序。
  • 如果您的 Effect 的不同部分应该因为不同的原因重新运行,请将其拆分为多个 Effect。
  • 如果您想根据先前的状态更新某些状态,请传递一个更新器函数。
  • 如果您想在不“响应”最新值的情况下读取它,请从您的 Effect 中提取一个 Effect Event。
  • 在 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>
}