Skip to content

Instantly share code, notes, and snippets.

@romgrk
Last active September 14, 2023 01:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save romgrk/cab5de0cc2a7cf1e2d87fea2ee360d07 to your computer and use it in GitHub Desktop.
Save romgrk/cab5de0cc2a7cf1e2d87fea2ee360d07 to your computer and use it in GitHub Desktop.
romgrk ranting about react hooks

I usually like to wrap React useEffect calls inside other functions, such as useOnMount, useOnChange, useOnceTrue, etc. I've often had pushback on that, and the argument is usually "it's another abstraction layer to learn", which is a fair point. I thought I'd summarize why I think this layer of abstraction is beneficial.

Semantic clarity

One of the reasons why we use Array methods like .filter, .map or .reduce instead of using a generic for (...) loop despite their lesser performance is that they provide a higher level of expressiveness, and they make the underlying logical operation much more semantically apparent.

For example, the two following snippets perform the same operation, but the second one makes it abundantly clear with just a glance that there is a filtering operation happening.

const newItems = []
for (let i = 0; i < items.length; i++) {
  if (items[i] < 0)
    newItems.push(items[i])
}
const newItems = items.filter(i => i < 0)

Similarly, using a hook derived from useEffect allows to bring a level of semantic clarity to the operation. The next two snippet both perform the same logic, but one of them allows me to know at a glance what is going on.

useEffect(() => {
  if (teamId !== undefined) {
    setLoaded(true)
  }
}, [teamId !== undefined])
useOnceTrue(teamId !== undefined, () => setLoaded(true))

Causality precedence

Another reason I do it is because I'm annoyed at the order of arguments for useEffect. The logical notation for effects should be cause -> effect! For some reason React decided to have effect then cause, and to make matters worst the effect is more often than not an inline closure, which means the cause can be separated by many lines from it's effect :(

// code as we read it with other constructs:
if (cause) {
  // effect
  // many lines of code
}
// code as react makes us write it:
useEffect(/* effect */ () => {
  // many lines of code
}, [cause])

The natural order is "if this, then that" or "when this changes, do that". As intended by God.

Even worst, most code editors will syntax-fold function calls as something like this:

useEffect(() => {...})

Which means the only way to find which effect one is looking for is to unfold it.

Final note

I'll admit that these readability benefits might seem marginal for small components, but they do really shine in more complex cases where there are many different effects inside a component. I hope this explains better why I've been doing it this way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment