DEV Community

Cover image for 8 Awesome React Hooks

8 Awesome React Hooks

Simon Holdorf on December 23, 2020

React.js is currently the most popular JavaScript library for front end developers. Invented by Facebook, but available as an open-source project, ...
Collapse
 
tiguchi profile image
Thomas Werner • Edited

Nice examples! I'm sure that you are aware of it, and know how to fix it. So for anyone else reading the article and my comment here: there's a small problem with the useFetch hook implementation.

It currently would not react to URL or options argument changes, since the internally used useEffect doesn't specify them as dependencies, so the response result wouldn't change should the request change (e.g. different URL or different query parameters). In fact fetch only runs once on component mount, because the dependencies array is empty.

In order to fix that the url and the options need to be added to the dependencies array as follows:

useEffect(() => {
    ...run fetch, store result...
}, [url, options]);
Enter fullscreen mode Exit fullscreen mode

Referential Equality Problem

The problem with React dependencies is that changes are detected through referential equality tests. So a hook with dependencies only gets re-run when the following is true for one or more of the dependency values:

before !== after
Enter fullscreen mode Exit fullscreen mode

That works fine for primitive values such as numbers, strings and booleans.

In case of functions or objects such as the options parameter this is a bit of a problem. Even when the data in an object literal does not change, two instances of {} are not referentially equal. JavaScript would create new instances for those object literals on each render run.

Three workarounds for that:

  1. Let the user of useFetch take care of memoizing the options via useMemo, so it's "referentially stable" and does not change from render run to render run. But that would be sorting out the problem from the wrong end in my opinion

  2. From within useFetch, serialize the options argument as string, and add that string to the dependencies array. The serialization must be stable though, so we don't get different results between render runs (i.e. different order of properties)

  3. Use an use effect hook that is able to do deep dependency comparisons. Let's call it useEffectDeep. That could be either an exercise for writing another custom hook, or can be downloaded from here (or other places): github.com/kentcdodds/use-deep-com...

Collapse
 
simonholdorf profile image
Simon Holdorf

Awesome, thank you very much for the detailed analysis!

Collapse
 
harisonfekadu profile image
harisonfekadu • Edited

🔥🔥

Collapse
 
eecolor profile image
EECOLOR

Great to see you are embracing the hook feature! And awesome you share your findings.

There are however a few small problems with your hooks.

useTimeout and useInterval

You are storing the callback into a ref which is great because you make sure you will allways call the latest version. You however set it using useEffect. This means if the callback changes, the change will be picked up in the next frame. Theoretically there is a chance that the callback has changed when the timeout (or interval) fires. You can fix it like this:

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

  React.useEffect(
    () => {
      if (delay !== null) return
      const id = setTimeout(() => callbackRef.current(), delay)
      return () => clearTimeout(id)
    },
    [delay]
  )
}
Enter fullscreen mode Exit fullscreen mode

usePrevious

This might actually be expected behavior, but this does not actually show the previous 'different' value. If you component rendered again with no value change usePrevious would return the same value. So the actual name should be useValueFromPreviousRender.

You could see this behavior when you add another state to the MoneyCount component that toggles a boolean when you click another button. The hook would be a bit more involved if you wanted to show the previous value after a change.

useClickInside and useClickOutside

This hook adds and removes the event listener on each render. It would be smart to use a similar strategy to useTimeout with a ref for the callback. On top of that, its a good idea to set the dependency array.

const callbackRef = React.useRef(null)
callbackRef.current = callback
React.useEffect(
  () => {
    window.addEventListener('click', handleClick)
    return () => window.removeEventListener('click', handleClick)
    function handleClick(e) {
      if (ref.current && ref.current.contains(e.target) callbackRef.current()
    }
  },
  []  
)
Enter fullscreen mode Exit fullscreen mode

As a side note: in theory the ref you are given could be a function ref, if that is the case your code would fail. The easiest way to guard for this is by creating and returning the ref from the hook itself.

useFetch

I think this is already mentioned but it only executes once during the lifetime of the component.

In real world applications you often want to know the response status code. I think it would be a great addition to add that.

useComponentDidMount and useComponentDidUnmount

These hooks capture the callback that is passed in. This means that if the callback changes, those changes will not be picked up. For useComponentDidMount this will most likely never cause a problem, for useComponentDidUnmount I can see this causing problems more often.

If someone in my company would create these hooks I would say that these hooks do not have the correct name. Their names refer to the component lifecycle and that is something hooks helps us to move away from. We want to focus on the actual effect and its lifecycle. On top of that, for new developers (that have only been exposed to React with hooks) the concepts of mount and unmount are unknown.


All in all great examples. I hope my comments are of value to you. I strongly suggest you install the eslint plugin as this would warn you for most problems.

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
Collapse
 
simonholdorf profile image
Simon Holdorf

Thank you very much for this detailed comment, this helps a lot!

Collapse
 
orenmizr profile image
Oren Mizrahi


blog.post.replace(/pictures/gi, 'code'));

Collapse
 
sunflower profile image
sunflowerseed

I think I would break the loading out of res.response in useFetch().
Why? Because if you load something else (a different URL), then res.response already exists and won't cause the "loading..." to be shown... so the loading flag really needs to be before the start of fetch() and after setData()

Collapse
 
sunflower profile image
sunflowerseed • Edited

useComponentWillUnmount this way (in the image), then won't it be true if the component is rendered 20 times, then useEffect() sets it up 20 times to run it 20 times? That's because you are calling useEffect() 20 times in total

Collapse
 
bernardbaker profile image
Bernard Baker

Hi, I enjoyed reading this. I didn't find use cases for all of them in a recent project. I'm using a Mediator pattern to handle fetch requests. I did repeat my self !DRY. But the abstraction is a refactor for another iteration.

I do agree with the comments which point out the errors. I would suggest making the fixes and updating this article. There's an option which allows you to do that.

And if you haven't already done so. Check out React Server Components. I'm sure you love them.

Collapse
 
arnoldgee profile image
Arnau Gómez

Hi Simon,
Thanks a lot for the examples ill definitely use them!
Do you have a repo where you store the code? If you only share images I cannot use them unless I type them again.

Collapse
 
simonholdorf profile image
Simon Holdorf

Hey, glad you like them. I wanted to try something different this time and animate others to actually do type it on their own. But I will prepare a short repo with the code!

Collapse
 
samselfridge profile image
samselfridge

Here's the one for useTimeout: codesandbox.io/s/youthful-spence-n...

You threw me off with the variable named seconds since it only increments every 5 seconds and then executes only once so I wanted to check it out.

Working code examples with posts like this would be helpful

Thread Thread
 
simonholdorf profile image
Simon Holdorf

Will make it that way next time 😊

Collapse
 
alexdanil profile image
AlexDanil

Hi. Nice article, thx a lot! Very useful for newer like me😊

Collapse
 
phtom profile image
phtom

ComponentDidMount and ComponentWillMount are now marked as unsafe to use on the latest version of React.JS

Collapse
 
adrai profile image
Adriano Raiano

The useTranslation hooks is awesome too 😉 dev.to/adrai/how-to-properly-inter...

Collapse
 
olaolu profile image
Olaoluwa Mustapha

Great article! These examples are really useful. For the useTimeout and useInterval hooks though, I think they can be simplified with the useCallback hook. Or was there a reason for not adding that?