Search code examples
react-reduxreact-hooksuse-effectuse-contextuse-ref

Where to define socket event listeners that require data from global state using Hooks in React


I've been learning hooks and one concept still really bogles me.

When using useEffect, any variables declared inside becomes old after the next re-render. To get access to changing values inside useEffect, the most common answer, and one Dan Abramov uses himself, is to use the useRef hook instead.

However, imagine that there is a piece of information that you want to store in a global state using something like Redux, but you also want that information available in callback functions inside useEffect. In my particular case, when my component mounts I need to add event listeners to a web socket connected to the server that signals WebRTC connections. The value that the web socket listener callback functions needs will be updated throughout the application's usage.

How do I organize a state that is globally accessible, but can also be referenced the same way that a ref made by useRef can be accessed?

Here's an example of what I mean

//App.js 
import React, {useEffect} from "react"
import {useSelector} from "react-redux"

import socketIOClient from "socket.io-client";

const App = () => {

   const users = useSelector(state => state.users)

   let socket 

   //when the component mounts, we establish a websocket connection and define the listeners 
   useEffect(() => {
      socket = socketIOClient(URL)

   //in my app in particular, when the user decides to broadcast video, they must make new RTCPeerConnections
   //to every user that has logged in to the application as well 
      socket.on("requestApproved", () => {
         //at this point, the users array is outdated
         users.forEach(callbackToCreateRTCPeerConnection)
      })
   }, [])

} 

When the client receives a response from the server that they can begin broadcasting, the client needs to have an accurate reflection of which users have logged in throughout their use of the app. Obviously, the value of users is stale at this point, because even the value from useSelector is not updated inside useEffect, although it is outside. So I could use useRef here to achieve what I want, but this isn't the only place where I use the users array, and I don't want to have to pass a ref down as props over and over again.

I have read about using useContext, but, if I understand correctly, when the context value changes, and the whole app is consuming the context, then a re-render is triggered for the entire app.

Any ideas, suggestions, explanations? Maybe there's a better place to add the event listeners to the sockets besides a useEffect?

Thanks in advance.


Solution

  • The idea about listeners is that they should be destroyed and recreated on closure value updates and cleaned up on unmount. You can add a users dependency to the useEffect and cleanup the listener

    const App = () => {
    
       const users = useSelector(state => state.users)
    
       let socket 
    
       //when the component mounts, we establish a websocket connection and define the listeners 
       useEffect(() => {
          socket = socketIOClient(URL)
    
       //in my app in particular, when the user decides to broadcast video, they must make new RTCPeerConnections
       //to every user that has logged in to the application as well 
          const listener = () => {
             //at this point, the users array is outdated
             users.forEach(callbackToCreateRTCPeerConnection)
          }
          socket.on("requestApproved", listener);
    
          return () => {
             socket.off("requestApproved", listener);
          }
    
       }, [users])
    
    }