A Critique of React Hooks

I want to preface this critique by saying I think that hooks are not all bad. If I were starting a new react project today I would still use them despite all these flaws. However, that doesn't make them immune to criticism. Given their design, I think there are a number of factors limiting their longevity and I won't be surprised if Facebook comes out with The Next Greatest Thing™ to address some of their shortcomings.

1. More Stuff to Learn

The first and easiest gripe with hooks is simply that they're another thing to learn. I was fortunate to get into frontend development at a simpler time where you could just dump jQuá on the page and call it a day. Then as new complications came along I could learn them incrementally. Nowadays there is just so       much       to       learn. If I was a new developer and saw that chart I would want to go home and rethink my life.

Learning new things is good though! We should be learning all the time! The problem with learning about hooks is that they're not generally applicable knowledge about computing or programming. They're a React-specific thing. In 5-10 years when you're programming something completely different your knowledge of hooks will be completely obsolete. Whenever possible, you should pick learning about generally applicable thing A over very specific thing B so that your knowledge can compound and pay dividends.

2. They Don't Interoperate With Class Components

If you're starting a new project that doesn't use class components you can GOTO 4, but read on if you're in the camp of working on an app from the time before hooks.

Say you're working in a project that's been around for a while and it's 50% FunctionComponent's and 50% class components. Now it's time to implement a new feature which you think would be a perfect use of a custom hook to do the heavy lifting. This works great in the original FunctionComponent that uses it, but now you need to use that feature in a class component. What do you do?

There are a few options, but they all have drawbacks. First, you could reimplement the feature without hooks but I hope you don't do that. Alternatively, you could create a HOC to wrap your component and pass down the hooks functionality. This creates a lot of boilerplate and doesn't compose well though. The only sustainable option is to suck it up and convert that class component to a FunctionComponent. Depending on how much stuff is going on in that component, this can take a while and there's a non-trivial chance of introducing a bug.

This is a problem that lessens in time as more of your app is converted of course, but can be a real challenge to being productive when each time you touch a component you have to refactor it.

3. Ecosystem Challenges

The React docs claim hooks are "Completely opt-in" and "you don't have to learn or use Hooks right now if you don't want to". This seems a little disingenuous to me. Code doesn't change but the world around it does.

As a library author, you're forced to make some choices in the now fractured ecosystem. Do you add support for hooks? Do you also maintain support for class components? For how long? This is a hard question to answer, especially if the library leaned heavily into HOC's.

As a library consumer, what happens when you need to upgrade a library but it made the choice to only support hooks? As the ecosystem evolves and adopts hooks you'll be coerced more and more into using them. This is another argument for reducing your dependencies but that's a story for another day.

4. The Rules of Hooks Limit Your Design

I'm assuming anyone who's read this far knows about the Rules of Hooks. The reason for the rules makes sense given that React uses call order to maintain state but this constraint can really limit how you organize and optimize your code.

In a recent example, I was building a hook that returns a lookup table where many of the values in the table were hooks themselves.

  function useLookupTable(param){
    return {
      a: {
        default: useADefault(param)
      },
      b: {
        default: useBDefault()
      },
      . . . many more entries
    }
  }

To avoid returning a big new object every time this hook is called I wanted to memoize the value. Lets try wrapping it in a useMemo:

  function useLookupTable(param){
    return useMemo(() => {
      return {
        a: {
          default: useADefault(param)
        },
        b: {
          default: useBDefault()
        },
        . . .
      }
    })
  }

Oops. React Hook "useADefault" cannot be called inside a callback. So let's pull all the hooks out and include them as dependencies:

  function useLookupTable(param){
    const aDefault = useADefault(param);
    const bDefault = useBDefault();
    . . .

    return useMemo(() => {
      return {
        a: {
          default: aDefault
        },
        b: {
          default: bDefault
        },
        . . .
      }
    }, [aDefault, bDefault, . . . ])
  }

We've now got rid of the error (and introduced an annoying number of temporary variables) but we're still returning a new table every time. Why? Oh, it's because one of the values is a new array which fails the dependency equality check every time. Enforcing each of these useXDefault hooks to return a stable value (like with a useRef) seems like a bad call since it would overly complicate them, and even if I did, as the table grows (and if each entry has multiple hooks) the dependency array soon gets out of control.

In this case I think the best solution is to refactor all these hooks to not be hooks and just regular functions that take their dependencies explicitly so that you can call them inline like in the second example and the dependency array for useMemo is manageable.

These sorts of battles with how you have to organize your code in a world of hooks is common, and it can take a lot of iteration, thought, and time to come up with a design that feels good.

5. They Complicate Control Flow

Using a few useState hooks is pretty easy to understand, but once you start needing to use all the other kinds of hooks up and down the component tree it becomes very difficult to follow the execution order of your code. How well can you reason about code if you don't understand the order it's executing in?

Here's a little quiz to see how well you can follow some toy examples.

If you aced it, congrats! I'm guessing you either ran them yourself or you had to sit back and scratch your head a bit. These are certainly contrived examples, but we all should be able to agree that simpler and less magical code is easier to work with than the alternative.

In Conclusion

I'm not asking you to never use hooks or remove them from your project. As I said in the intro, I would still use hooks in a new project, and I think they do improve the prior situation of HOC's creating component hierarchies deeper than the Mariana Trench. But I also think they're not the "Computing Promised Land". It might be a while before we get there though.

So until then, just use them judiciously.

P.S. Library authors, please try to keep things simple!

Cross posted to https://medium.com/indigoag-eng/a-critique-of-react-hooks-6de10e8f14e1

Quiz results and a few more thoughts in the addendum!

Back to home More blog posts

Comments

comments powered by Disqus