Using refs to manage callbacks between React and JS objects

44 Views Asked by At

Let's say I have the following code:

function MyComponent() {
  const [status, setStatus] = useState('status_a')
  const [socket, setSocket] = useState()

  useEffect(() => {
    const socket = new WebSocket('ws://foo.com')
    socket.onclose = () => console.log(`socket closed while status was ${status}`)
    setSocket(socket)
  }, [])

  // ... some code that changes status in response to user input
}

So there's a bug here, because when socket.onclose fires, status will always be the initial value, "status_a".

One solution, using refs:

function MyComponent() {
  const [status, setStatus] = useState('status_a')
  const [socket, setSocket] = useState()
  const callbacks = useRef({})

  useEffect(() => {
    const socket = new WebSocket('ws://foo.com')
    socket.onclose = () => callbacks.current.socketOnClose()
    setSocket(socket)
  }, [])

  // ... some code that changes status in response to user input

  // this gets redefined on every render, so it always has the new value of status
  callbacks.current.socketOnClose = () => {
    console.log(`socket closed while status was ${status}`)
  }
}

It works, but smells a bit weird to me. Any better ideas?

This example only has one state dependency in the callback, but the real code has many dependencies, so setting up a change handler for each one that redefines the callback would also be clunky.

1

There are 1 best solutions below

1
Danziger On

I think using a ref is the right solution, but I just don't think you should use it for the callback, but for the values you want the callback to have access to:

function MyComponent() {
  const [status, setStatus] = useState('status_a')
  const [socket, setSocket] = useState()

  const statusRef = useRef(status)

  useEffect(() => {
    statusRef.current = status
  }, [status])

  useEffect(() => {
    const socket = new WebSocket('ws://foo.com')

    socket.onclose = () => console.log(`socket closed while status was ${statusRef .current}`)

    setSocket(socket)
  }, [])

  // ... some code that changes status in response to user input
}

Why? Let's see what the React documentation says:

Referencing Values with Refs:

When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref.

This is similar to your case where your onclose callback, and therefore the useEffect that contains it, both have a dependency on some data (status), but you do not want a change on that data to re-run your useEffect and socket setup logic.