A Critique of React Hooks Addendum

This is a follow up to A Critique of React Hooks which blew up beyond expectations and generated a ton of discussion that I enjoyed reading. If you haven't read it yet please take the detour before spoiling the quiz for yourself! I'm not one to harp on a subject repeatedly but after seeing all the quiz results come in I thought it would be valuable to share them as well as a couple of thoughts across common themes in the discussion.

Quiz Results

With over 2,600 responses at the time of writing this is the biggest sample size of an unscientific quiz I've ever given! Even ignoring the last question which was mostly there for laughs, the results were still very interesting. The median score is 1 out of 4 questions despite 3/4ths of the questions having a correct popular vote. To me this indicates that people are familiar with a few of the hooks but haven't used all the ones included in the quiz.


Quesiton 1

Question 1 code
Question 1 results

The first and simplest question had the highest percentage of correct answers to no surprise. However, over 40% of you were surprised that the Child's useEffect fires before the Parent's after rendering both components.

Question 2

Question 2 code
Question 2 results

This one surprised me when I first ran it but makes sense when you take a minute to think about it. Both useLayoutEffect's run before the useEffect's and they run from the bottom of the component tree upwards. Hopefully you'll never get into a situation where this particular ordering matters though since only 30% were able to predict the order at first glance.

Question 3

Question 3 code
Question 3 results

This question has the intentional bug of creating a new object every rerender and passing it to the child as a prop which causes the memo to run every rerender which a third of you caught. It would be interesting to compare these results with a question around a similar bug of creating a function inside the render and passing it to the child which causes the Child to rerender every time the Parent does:

function Parent(props) {
  function doSomething() {
    // … props …

  return <Child onSomething={doSomething} />

This is fixed with a useCallback hook, and the quiz problem is fixed by either updating the object via useRef or refactoring the props of the child to not take an object as a prop. Both of these scenarios can come from refactoring a class component where you either have a class method or property that don't get recreated every render. When converting you have to keep this in mind and take the obfuscation penalty wrapping them with hooks.

Question 4

Question 4 code
Question 4 results

This is the opposite sort of bug where a Child component needs to be rerendered but isn't. It happens when useRef is needed to maintain a stable reference over rerenders but then a child component is added that needs the value. If the whole ref is passed as a prop instead of the .current value the Child component won't be rerendered because the ref hasn't changed. In isolation this shouldn't be too hard to spot but can be tricky when there are many props being passed from different places to a component.

A few more thoughts

On More Stuff to Learn

There were a few comments about how easy hooks are to learn. While I would agree that reading the docs and getting the gist of how hooks work doesn't take too long, fully understanding their ramifications, and the bugs you'll run into, will be an ongoing process. The quiz results show that even if you got everything right, you'll likely be working with someone that didn't.

Adding more to learn just raises the barrier to entry. And every developer should be trying to remove as many barriers to entry because it's in our best interest to do so.

They Don't Compose with Themselves

Programming languages are powerful because they give you building blocks that you can compose together in many different ways. If you have the blocks loop and function, all the possible combinations are valid including nesting. loop in loop, loop in function, function in loop, etc. Hooks don't have this property. You can't use a hook inside a hook (excluding custom hooks that are just wrapper functions for the native hooks). You can't use a hook in a loop. So when you go from having one thing handled with useRef to needing multiple things handled by useRef you can't just stick it in a loop or array like you would with any other language building block. You have to redesign it to work with hooks.

Why Control Flow Matters

When you inevitably get to debugging, you should start with a mental model of what you expect to happen including all the assumptions about what state you'll be in throughout the process. Whenever you step or log your way through and one of your assumptions is broken, you know you've found a piece of the bug. Now you just have to figure out why your assumption was wrong.

Between the hook execution order and figuring out why a component rerendered it can be very difficult to create that mental model of what's supposed to happen.

Last Words

Whether you like hooks or not, I hope through these posts you've learned something even if it isn't as general as I'd like :)

As always,
Thanks for reading and discussing!


comments powered by Disqus