DEV Community

Discussion on: 8 Awesome React Hooks

Collapse
 
codebrewer profile image
wangyouhua

Nice comment! But I have a puzzle: why borther to use

const callbackRef = React.useRef(null);
callbackRef.current = callback
Enter fullscreen mode Exit fullscreen mode

I think we can eliminate the use of useRef, just use the callback argument in the place callbackRef.current.

Collapse
 
eecolor profile image
EECOLOR

@wangyouhua Great question: What is the difference between these versions?

function useTimeout1(callback, timeout) {
  const callbackRef = React.useRef(null)
  callbackRef.current = callback

  React.useEffect(
    () => {
      if (!timeout && timeout !== 0) return
      const id = setTimeout(() => callbackRef.current(), timeout)
      return () => clearTimeout(id)
    },
    [timeout]
  )
}

// vs

function useTimeout2(callback, timeout) {
  React.useEffect(
    () => {
      if (!timeout && timeout !== 0) return
      const id = setTimeout(() => { callback() }, timeout)
      return () => clearTimeout(id)
    },
    [timeout] // you will get a lint warning here about `callback`
  )
}

// vs

function useTimeout3(callback, timeout) {
  React.useEffect(
    () => {
      if (!timeout && timeout !== 0) return
      const id = setTimeout(() => { callback() }, timeout)
      return () => clearTimeout(id)
    },
    [timeout, callback]
  )
}
Enter fullscreen mode Exit fullscreen mode

You can see the difference in the usage, in this example I want to log the value of someState after 5 seconds:

const [someState, setSomeState] = React.useState(0)
useTimeout1(
  () => { console.log(someState) },
  5000
) // will correctly log the value of `someState` after 5 seconds

// vs

const [someState, setSomeState] = React.useState(0)
const callback = React.useCallback(() => { console.log(someState) }, someState)
useTimeout2(
  callback,
  5000
) // this will always log 0, even if `someState` changes 

// vs

const [someState, setSomeState] = React.useState(0)
const callback = React.useCallback(() => { console.log(someState) }, someState)
useTimeout3(
  callback,
  5000
) // when `someState` changes, you need to wait another 5 seconds to see the results
Enter fullscreen mode Exit fullscreen mode

As you can see version 2 and 3 have a bit different behavior. Version 2 is broken (and caught by the linting rules). Version 3 has unexpected behavior, it resets the timeout when someState is changed.

This has to with the 'capturing' of the callback. Once you pass a function to setTimeout that function can not be changed. Primitive values it references are captured. In order to get the expected result and don't put the burden of that correct handling on the user of the hook, we keep a reference to the 'current' callback.

So if you are using callbacks in hooks this is a great pattern:

const callbackRef = React.useRef(null)
callbackRef.current = callback

useSomeAsynchronousHook(
  () => { someAsynchrounousMethod(() => { callBackRef.current() }) },
  [] // there is no longer a dependency on the callback
)
Enter fullscreen mode Exit fullscreen mode