如何在只运行一次的 useEffect 中使用上下文值

How to use context values in useEffect, that only runs once

我遇到了一个有趣的问题。我正在构建一个使用与服务器的网络套接字通信的反应应用程序。我在 useEffect 挂钩中创建了这个 websocket,因此不能 运行 多次,否则我最终会得到多个连接。但是,在这个 useEffect 中,我打算使用一些变量,这些变量实际上位于上下文 (useContext) 挂钩中。当上下文值发生变化时, useEffect 中的值不会更新,这是可以理解的。我试过 useRef,但没有用。你有什么想法吗?

const ws = useRef<WebSocket>();

  useEffect(() => {
    ws.current = new WebSocket("ws://localhost:5000");
    ws.current.addEventListener("open", () => {
      console.log("opened connection");
    });

    ws.current.addEventListener("message", (message) => {
      const messageData: ResponseData = JSON.parse(message.data);
      const { response, reload } = messageData;

      if (typeof response === "string") {
        const event = new CustomEvent<ResponseData>(response, {
          detail: messageData,
        });
        ws.current?.dispatchEvent(event);
      } else {
        if (reload !== undefined) {
          console.log("general info should reload now");
          GeneralInfoContext.reload(reload);
        }
        console.log(messageData);
      }
    });
  });

Web 套接字存储为 ref,以便更好地用于此 useEffect 块之外的不同功能

注意:要使用的上下文值其实是一个函数,GeneralInfoContext.reload()

你应该将一个空数组作为第二个参数传递给 useEffect,所以在这种情况下它变得类似于 react

的 componentDidMount() 逻辑
useEffect(() => { 
 ...your websocket code here
}, [])

您可以向 useEffect 提供变量列表,当这些变量发生变化时,useEffect 将重新运行。

这是一个小例子:

const [exampleState, setExampleState] = useState<boolean>(false);

useEffect(() => {
  console.log("exampleState was updated.");
}, [exampleState]);
  

来自 reactjs 网站的示例:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

拆分useEffect的解决方案

您可以将打开 websocket 连接的逻辑与将消息处理程序添加到单独的 useEffect 中的逻辑分开 - 第一个可以 运行 一次,而第二个可以重新附加每次依赖项更改时的事件:

useEffect(() => {
    ws.current = new WebSocket("ws://localhost:5000");
    ws.current.addEventListener("open", () => {
        console.log("opened connection");
    });
}, []);

useEffect(() => {
    const socket = ws.current;
    if(!socket) throw new Error("Expected to have a websocket instance");
    const handler = (message) => {
        /*...*/
    }
    socket.addEventListener("message", handler);
    // cleanup
    return () => socket.removeEventListener("message", handler);
}, [/* deps here*/])

效果将 运行 按顺序排列,因此第二个效果将在第一个效果设置 ws.current.

后 运行

回调参考的解决方案

或者,您可以将处理程序放入 ref 并根据需要更新它,并在调用事件时引用 ref:

const handlerRef = useRef(() => {})

useEffect(() => {
    handlerRef.current = (message) => {
        /*...*/
    }
    // No deps here, can update the function on every render
});

useEffect(() => {
    ws.current = new WebSocket("ws://localhost:5000");
    ws.current.addEventListener("open", () => {
        console.log("opened connection");
    });

    const handlerFunc = (message) => handlerRef.current(message);
    ws.current.addEventListener("message", handlerFunc);
    return () => ws.current.removeEventListener("message", handlerFunc);
}, []);

重要的是你不要这样做 addEventListener("message", handlerRef.current) 因为那样只会附加函数的原始版本 - 额外的 (message) => handlerRef.current(message) 包装器是必要的,这样每条消息都会传递到最新版本处理函数的

这种方法仍然需要两个 useEffect,因为最好不要将 handlerRef.current = /* func */ 直接放在渲染逻辑中,因为渲染不应该有副作用。


使用哪个?

我个人喜欢第一个,分离和重新附加事件处理程序应该是无害的(基本上 'free')并且感觉比添加额外的引用更简单。

但是第二个避免了对显式依赖项列表的需要,这也很好,特别是如果您不使用 eslint 规则来确保详尽的依赖项。 (虽然你绝对应该)