Как использовать контекстные значения в useEffect, который запускается только один раз

#reactjs #react-hooks #use-effect #use-context

Вопрос:

у меня тут интересная проблема. Я создаю приложение react, используя связь веб — сокета с сервером. Я создаю этот веб-сайт в крючке useEffect, который, следовательно, не может выполняться несколько раз, иначе у меня было бы несколько подключений. Однако в этом эффекте использования я намерен использовать некоторые переменные, которые на самом деле находятся в контексте (useContext). И когда значения контекста меняются, значения в useEffect , по понятным причинам, не обновляются. Я пробовал использовать useRef, но это не сработало. У тебя есть какие-нибудь идеи?

 const ws = useReflt;WebSocketgt;();   useEffect(() =gt; {  ws.current = new WebSocket("ws://localhost:5000");  ws.current.addEventListener("open", () =gt; {  console.log("opened connection");  });   ws.current.addEventListener("message", (message) =gt; {  const messageData: ResponseData = JSON.parse(message.data);  const { response, reload } = messageData;   if (typeof response === "string") {  const event = new CustomEventlt;ResponseDatagt;(response, {  detail: messageData,  });  ws.current?.dispatchEvent(event);  } else {  if (reload !== undefined) {  console.log("general info should reload now");  GeneralInfoContext.reload(reload);  }  console.log(messageData);  }  });  });  

Веб — сокет хранится как ссылка для лучшего использования в различных функциях за пределами этого блока useEffect

Примечание: используемое значение контекста на самом деле является функцией GeneralInfoContext.reload()

Ответ №1:

Решение с эффектом раздельного использования

Вы можете разделить логику, которая открывает подключение websocket, на две части тот, который добавляет обработчик сообщений в отдельный useEffect s — первый может выполняться один раз, в то время как второй может повторно присоединять событие каждый раз, когда меняется зависимость:

 useEffect(() =gt; {  ws.current = new WebSocket("ws://localhost:5000");  ws.current.addEventListener("open", () =gt; {  console.log("opened connection");  }); }, []);  useEffect(() =gt; {  const socket = ws.current;  if(!socket) throw new Error("Expected to have a websocket instance");  const handler = (message) =gt; {  /*...*/  }  socket.addEventListener("message", handler);  // cleanup  return () =gt; socket.removeEventListener("message", handler); }, [/* deps here*/])  

Эффекты будут выполняться по порядку, поэтому второй эффект будет выполняться после того, как первый эффект уже установлен ws.current .


Решение с обратным вызовом ref

В качестве альтернативы вы можете поместить обработчик в ссылку и обновить его по мере необходимости, а также ссылаться на ссылку при вызове события:

 const handlerRef = useRef(() =gt; {})  useEffect(() =gt; {  handlerRef.current = (message) =gt; {  /*...*/  }  // No deps here, can update the function on every render });  useEffect(() =gt; {  ws.current = new WebSocket("ws://localhost:5000");  ws.current.addEventListener("open", () =gt; {  console.log("opened connection");  });   const handlerFunc = (message) =gt; handlerRef.current(message);  ws.current.addEventListener("message", handlerFunc);  return () =gt; ws.current.removeEventListener("message", handlerFunc); }, []);  

Важно, чтобы вы этого не делали addEventListener("message", handlerRef.current) , так как это приведет только к прикреплению исходной версии функции — дополнительная (message) =gt; handlerRef.current(message) оболочка необходима, чтобы каждое сообщение передавалось в последнюю версию функции обработчика.

Этот подход по-прежнему требует двух useEffect , так как лучше не вводить handlerRef.current = /* func */ их непосредственно в логику рендеринга, так как рендеринг не должен иметь побочных эффектов.


Что использовать?

Лично мне нравится первый вариант, отсоединение и повторное подключение обработчиков событий должно быть безвредным (и в основном «бесплатным») и кажется менее сложным, чем добавление дополнительной ссылки.

Но второй вариант позволяет избежать необходимости в явном списке зависимостей, что тоже неплохо, особенно если вы не используете правило eslint для обеспечения исчерпывающих deps. (Хотя вы определенно должны быть)

Ответ №2:

Вы можете предоставить useEffect список переменных и useEffect запустить его повторно, когда эти переменные изменятся.

Это небольшой пример:

 const [exampleState, setExampleState] = useStatelt;booleangt;(false);  useEffect(() =gt; {  console.log("exampleState was updated."); }, [exampleState]);    

Пример с сайта reactjs:

 useEffect(() =gt; {  function handleStatusChange(status) {  setIsOnline(status.isOnline);  }   ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);  return () =gt; {  ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);  }; }, [props.friend.id]); // Only re-subscribe if props.friend.id changes  

Комментарии:

1. Ну, я попытался добавить этот массив, попытался включить как GeneralInfoContext, так и .перезагрузка, но этот эффект все еще не срабатывает при каждом изменении, и я продолжаю получать значение функции по умолчанию (я знаю это, потому что я ввел в него журнал консоли, чтобы я мог отслеживать)

Ответ №3:

Вы должны передать пустой массив в качестве второго параметра в useEffect, так что в этом случае он становится похожим на логику componentDidMount() react

 useEffect(() =gt; {   ...your websocket code here }, [])