32

The ugly side of React Hooks

 3 years ago
source link: https://medium.com/@dominictobias/the-ugly-side-of-react-hooks-86ee52c160ab
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

iEJzyij.jpg!web

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?

I’m sure you’ve spotted a few mistakes, let’s go over them:

  • We have a dependency on attachDocEvents and removeDocEvent in the callbacks
  • That means we also have to memoize attachDocEvents and removeDocEvents . Oh and since attach/removeDocEvents reference those callbacks we need to make them dependencies too
  • Bonus: useEffect which is only supposed to be called on unmount will be called every render since the removeDocEvents reference changes

All those considerations we didn’t previously have. Let’s try to fix them:

Okay that’s better. But there’s still a problem. Unlike classes or a normal function , const functions can only be referenced from top-down. So we cannot reference handleDragMove or handleDragComplete before they are defined. But if we move them above then they cannot reference 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

Ms. Hookmeister wants to use a third party physics library for some cool effects. He knows he doesn’t need to re-run it so since he’s a hook expert he uses useRef instead of useMemo .

Now particlePhysics holds a constant reference to the expensive class he just instantiated, and he would be correct.

What he doesn’t know is that even though he 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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK