The ugly side of React Hooks

Dominic Tobias

Jul 24 · 4 min read

Image for post

Everyone’s using React Hooks these days, functions are in and classes are out. No more this yay! (One of the reasons the React team purportedly introduced them). They allow us to easily “mixin” functionality into our component and elegantly execute code when dependencies change. They’re a nice idea.

But sadly we’ve traded “cool” for performance, and simple solvable problems like this context for much more nuanced and complicated ones.

Stale memory

A problem we rarely had to worry about in the past, but take a simple callback which greets a person:

If we later change greeting the callback is now referencing an old value. If this was a 2mb image that would also be a 2mb memory leak. Hooks have introduced one of the two hard things about programming: cache invalidation.

Infinite loops

“Add it as a dependency” the hook experts say. So let’s fetch a random number and set it to state:

Now the fetchRandomNumber function changes after every fetch and re-runs in an infinite loop.

This seems contrived but happens in any situation where you want to both read and set state in a callback which executes on mount, such as fetching some data when your page loads.

Slower render reconciliation

Most code I’ve seen rarely wraps functional components in React.memo and we’re encouraged to write naked functions inside render which will break any purity checks as they create a new function every render.

We’re told not to prematurely optimize and that it’s probably “fast enough”, because dealing with memoizing all your functions is not pretty, more costly to initialize and execute, and not trivial due to gotchas that require expert knowledge of JavaScript and hooks (we’ll get to this).

When you create methods or variables in classes they are created once per instance. It doesn’t require someone with deep knowledge of JavaScript to combine this with PureComponent to have a very efficiently updating app.

For any callbacks you do handeClick = e => { ... } and voila you’ve both solved any confusion around this and also have a “memoized” callback which didn’t involve the nightmares of useCallback.

And this gain which many people tell you not to worry about really can make a significant difference in large apps or components which render lots of elements like a data grid. In my test on a virtual grid it was over 20x faster to render memoized cells for example.

The useCallback Catch-22

We’ll just use useCallback you say. So let’s create a resize handle. In a class component the events for mouse would look something like this:

Now let’s do the same for a hook, should be simple right?

  • The handlers need to be a constant reference to be removed (and not interrupted during a drag)
  • The attach/remove handlers also have to be wrapped in useCallback not break the dependency checks
  • Bonus: removeDocEvents also needs to be constant else it will be removed every render instead of only on unmount

Phew so much domain knowledge we didn’t previously need!

But there’s still a problem. Unlike classes or a normal function, const functions can only be referenced from top-down. So we can’t reference handleDragMove or handleDragComplete before they’re defined. But if we move them above then they can’t reference in attach/removeDocEvents… Do you know how to solve that?

Now your average developer who could write an efficient React app with classes needs to be both an expert in JavaScript to understand function hoisting and reference passing, and they also need deep domain specific knowledge of React Hooks:

We simply had to “tunnel” in references to the callbacks, lovely! If you knew how to solve this you might think you’re a bonafide useRef expert. So how about this…

So you think you can useRef

You want to use a third party physics library for some cool effects. It doesn’t need to change so you useuseRef instead of useMemo :

What you might not know is that even though particlePhysics always holds the first instance, a new instance is being created every render and discarded. That’s simply the nature of how JS executes. Again, requiring expert understanding to avoid expensive pitfalls.

If you don’t believe me give this a run and check the console:

All this bashing but didn’t I say hooks are a nice idea?

Totally, they can produce some clean looking code. Before hooks we didn’t have such an elegant way to mix in and re-use functionality, and we also saw a lot of ugly code in componentDidUpdate or getDerivedStateFromProps to well… derive state from props, but don’t forget that memoization isn’t even a necessity half the time with classes and you can also easily memoize functions and variables in them too.

Hooks are a clever hack to achieve a vision at the expense of API surface area, deeper JS expertise, performance and sometimes complexity. They move the React ecosystem even further away from web standards. Now we are seeing a proliferation of code which can’t even run on one half of React components let alone in a normal JS function.

So are React Hooks the way forward? Let me know what you think!

Author: admin

Leave a Reply

Your email address will not be published.