如何在只运行一次的 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 规则来确保详尽的依赖项。 (虽然你绝对应该)
我遇到了一个有趣的问题。我正在构建一个使用与服务器的网络套接字通信的反应应用程序。我在 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 规则来确保详尽的依赖项。 (虽然你绝对应该)