第四二章 逃生舱口-删除 Effect 依赖项

2023-02-28  本文已影响0人  深圳都这么冷

删除 Effect 依赖项

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

你将学习

  • 如何修复无限效果依赖循环
  • 当你想删除依赖项时该怎么做
  • 如何从 Effect 中读取值而不对其做出“反应”
  • 如何以及为什么要避免对象和函数依赖
  • 为什么抑制依赖 linter 是危险的,应该怎么做

依赖项应与代码匹配

当您编写 Effect 时,您首先指定如何开始和停止您希望 Effect 执行的操作:

const serverUrl = 'https://localhost: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://localhost: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} />
    </>
  );
}
Lint Error
11:6 - React Hook useEffect has a missing dependency: 'roomId'. Either include it or remove the dependency array.

根据 linter 所说的填写它们:

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

效果对反应值“反应”。 由于 roomId 是一个反应值(它可能会因重新渲染而改变),linter 会验证您是否已将其指定为依赖项。 如果 roomId 接收到不同的值,React 将重新同步您的 Effect。 这确保聊天保持连接到所选房间并对下拉列表“做出反应”.

要删除依赖项,请证明它不是依赖项

请注意,您无法“选择”效果的依赖项。 您的 Effect 代码使用的每个反应值都必须在您的依赖列表中声明。 您的 Effect 的依赖列表由周围的代码决定:

const serverUrl = 'https://localhost: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://localhost: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://localhost: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://localhost: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 或状态改变时重新运行。

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

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

  1. 首先,您更改 Effect 的代码或声明反应值的方式。
  2. 然后,您按照 linter 并调整依赖项以匹配您更改的代码。
  3. 如果您对依赖项列表不满意,请返回第一步(并再次更改代码)。

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

这可能感觉像是在求解方程式。 您可能从一个目标开始(例如,删除依赖项),然后您需要“找到”与该目标匹配的确切代码。 不是每个人都觉得解方程式很有趣,写 Effects 也是一样! 幸运的是,下面列出了您可以尝试的常见食谱。

"陷阱

如果你有一个现有的代码库,你可能有一些抑制 linter 的效果,如下所示:

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 函数。 在该渲染期间,计数为 0,增量为 1。这就是为什么该渲染中的 onTick 总是每秒调用 setCount(0 + 1),而您总是看到 1。这样的错误在分布在多个时更难修复 成分。

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

我们建议将依赖性 lint 错误视为编译错误。 如果你不压制它,你永远不会看到这样的错误。 本页的其余部分记录了这种情况和其他情况的备选方案。

删除不必要的依赖项

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

要找到正确的解决方案,您需要回答几个关于您的Effect的问题。 让我们来看看它们。

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

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

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

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

  // ...
}

但是通过这样做,你引入了一个错误。 想象一下,您先提交表单,然后在深色和浅色主题之间切换。 主题会改变,效果会重新运行,所以它会再次显示相同的通知!

这里的问题是,这首先不应该是一个 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 是否在做几件不相关的事情。

想象一下,您正在创建一个运输form,用户需要在其中选择他们的城市和地区。 根据所选国家/地区从服务器获取城市列表,以便将它们显示为下拉选项:

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 中获取数据的一个很好的例子。 您正在根据 country prop 将城市状态与网络同步。 您不能在事件处理程序中执行此操作,因为您需要在显示 ShippingForm 时以及国家/地区发生变化时(无论是哪种交互导致它)立即获取。

现在假设您要为城市区域添加第二个选择框,它应该获取当前所选城市的区域。 您可以首先为同一 Effect 中的区域列表添加第二个获取调用:

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. 你想根据 country 属性将城市状态同步到网络。
  2. 您希望根据城市状态将区域状态同步到网络。

将逻辑拆分为两个 Effect,每个 Effect 都对需要同步的 prop 做出反应:

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 在城市发生变化时重新运行。 您已按目的将它们分开:两个不同的事物由两个独立的效果器同步。 两个独立的 Effects 有两个独立的依赖列表,因此它们不会再无意中相互触发。

最终的代码比原来的要长一些,但是拆分这些Effects还是正确的。 每个 Effect 应该代表一个独立的同步过程。 在此示例中,删除一个 Effect 不会破坏另一个 Effect 的逻辑。 这很好地表明它们同步了不同的事物,将它们分开是有意义的。 如果重复令人担忧,您可以通过将重复逻辑提取到自定义 Hook 中来进一步改进此代码。

你在读一些状态来计算下一个状态吗?

每次新消息到达时,此 Effect 都会使用新创建的数组更新消息状态变量:

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

使消息成为依赖项会引入一个问题。

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

要解决此问题,请不要阅读 Effect 中的消息。 相反,将更新程序函数传递给 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 本身不再需要依赖消息的原因。 由于此修复,接收聊天消息将不再使聊天重新连接。

你想读取一个值而不对其变化做出“反应”吗?

构建中

本节描述了一个尚未添加到 React 中的实验性 API,因此您还不能使用它。

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

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 中的更改做出“反应”。 将这段非反应性逻辑移到效果事件中:

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 Events 让您可以将 Effect 分成反应部分(应该“反应”反应值,如 roomId 及其变化)和非反应部分(只读取它们的最新值,如 onMessage 读取 isMuted)。 现在您在 Effect Event 中读取了 isMuted,它不需要是您的 Effect 的依赖项。 因此,当您打开和关闭“静音”设置时,聊天不会重新连接,解决了原始问题!

从属性包装事件处理程序

当您的组件接收事件处理程序作为 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
  // ...

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

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

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

解决方案还是将非反应性代码拆分成一个 Effect Event:

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

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

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

一些反应值是否会无意中改变?

有时,您确实希望您的 Effect 对某个值做出“反应”,但该值的变化比您希望的更频繁——并且可能不会从用户的角度反映任何实际变化。 例如,假设您在组件主体中创建了一个选项对象,然后从 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 将使用新选项重新连接到聊天。 但是,上面的代码也有问题。 要查看问题,请尝试在下面的沙箱中输入内容,然后观察控制台中发生的情况:
chat.js

export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

App.js

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

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

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

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

Lint Error
9:9 - The 'options' object makes the dependencies of useEffect Hook (at line 18) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'options' in its own useMemo() Hook.

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

这意味着在 ChatRoom 组件的每次重新渲染时都会从头开始创建一个新的选项对象。 React 发现选项对象与上次渲染期间创建的选项对象不同。 这就是为什么它会重新同步您的效果(取决于选项),并且会在您键入时重新连接聊天。

此问题特别影响对象和函数。 在 JavaScript 中,每个新创建的对象和函数都被认为与其他所有对象和函数不同。 里面的内容可能是一样的也没关系!

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

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

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

对象和函数依赖性会产生风险,即您的 Effect 将比您需要的更频繁地重新同步。

这就是为什么您应该尽可能避免将对象和函数作为 Effect 的依赖项。 相反,尝试将它们移到组件外部、Effect 内部,或从中提取原始值。

将静态对象和函数移出组件
如果该对象不依赖于任何 props 和状态,您可以将该对象移到您的组件之外:

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

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

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

这样,您就可以向 linter 证明它不是反应性的。 它不会因为重新渲染而改变,所以它不需要依赖于你的 Effect。 现在重新渲染 ChatRoom 不会导致您的 Effect 重新同步。

这也适用于函数:

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

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

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

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

在 Effect 中移动动态对象和函数

如果你的对象依赖于一些可能会因重新渲染而改变的反应值,比如 roomId 属性,你不能将它拉到你的组件之外。 但是,您可以将其创建移动到 Effect 的代码中:

const serverUrl = 'https://localhost: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
  // ...

现在选项已在您的 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

由于此修复,如果您编辑输入,聊天将不再重新连接:
chat.js

export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

App.js

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

const serverUrl = 'https://localhost: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://localhost: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.roomId 或 options.serverUrl 实际发生变化,聊天将按照您的预期重新连接。

从函数计算原始值

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

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

为避免使其成为依赖项(从而导致它在重新渲染时重新连接),请在 Effect 外部调用它。 这为您提供了不是对象的 roomId 和 serverUrl 值,您可以从 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 中。

回顾

上一篇 下一篇

猜你喜欢

热点阅读