The whole React world has moved on to hooks and I’m left here sitting thinking, “wait, you guys think this is a good API?” I know this is considered blasphemy in the React community but let me explain. Hooks by themselves are not a bad idea. My problems mostly lie with the implementation and the restrictions they impose along with a blatant disregard for performance. If a developer came to me with an API that had the same restrictions and caveats as hooks I would tell them to rewrite it. Vue 3 is that rewrite.

React has you put your JSX in the same function that you put your hooks. No matter the framework, template logic needs to run on every render. Most of the warts around React’s hooks come from the component effectively being redeclared every render and trying to compensate for the problems that imposes. There is no reason to redeclare components on every render. Initialization values should only be created on initialization.

How this affects usability

Lets say someone came to you and said they have a great new API. They tell you “just don’t use it in any loops or conditionals or the whole thing breaks”. Would that sound like something you want to base your whole app around? This is a fundamental restriction with React hooks. You can’t have any hooks executing inside conditionals or loops. You can’t skip hooks. You can’t return early without executing all the hooks even if you don’t need them. This also means you can’t put hooks inside other hooks. Every hook must execute in the exact same order on every render. As an API purported to be better because it is functional, the only way that this is functional because it uses functions.

Hooks work by reaching out into global state and getting a reference to the component that is currently being worked on. On the first render React’s hooks get put into an array based on the order they are called. That is how React knows which hook you are dealing with on subsequent renders. Storing the call order in an array is the root of all the problem with React’s hooks.

How this affects performance

React and Vue both watch state. When state changes, the components that were changed and all of their decedent components are checked for changes. That generates a virtual DOM tree which is then diffed with the instance of the virtual DOM tree that was created on the previous render. Any changes between the two instances are persisted to the real DOM.

For a hook to be useful it must have access to your component’s state. For it to have access to your component’s state, it must be declared in your component. Anything that is declared inside a React functional component is being declared whenever that component renders. Since we store things like form values in state, that means that on every keypress, every function and variable that makes up the components that have changed is being redeclared.

When you write

const [value, setValue] = useState({bar: foo})

{bar: foo} is the initialization value. You are declaring {bar: foo} on every render. On every render except the first render before the component mounts, the object {bar: foo} is simply thrown away.

If that value is expensive to compute React lets you pass a function instead.

When you write

const [value, setValue] = useState(() => someFunction(someDependency))

You are still declaring a function on every render. It is going to be thrown away on every render except first render. When you write

useCallback(() => { // do something }, [a, b])

You are declaring a function and an array on every render. The array is then compared to the array that was given on the last render. If the values are the same the function and the array are simply thrown away. With Vue 3 all of this work of recreating the function on every render while creating and checking the dependency array does not need to be done.

useCallback is fundamentally a workaround to make sure memoization continues to work in a hooks world. It is a hack to keep a function from changing when it shouldn’t change. It does this by creating a new function and throwing it away if it is not needed. Anytime anything in your app changes, all these functions, dependency arrays, and initialization values are recreated. It’s more work for the garbage collector.

“But JavaScript is fast so it doesn’t matter if we redeclare things”

The React team seems to operate under the notion that JavaScript executes for free and everything that makes web slow comes from compositing and rendering. They address this exact issue on their hooks FAQ and state that

In modern browsers, the raw performance of closures compared to classes doesn’t differ significantly except in extreme scenarios.

There is a lot of careful sidestepping in this part of the FAQ. We aren’t comparing if closures are faster than classes. Compare the cost of redeclaring all of your functions, initialization variables, and dependency arrays on every single render and throwing them away with the cost of not doing that.

If we benchmark the load time of this simple React FAQ page that is server side rendered we can see that it spends over half the time in scripting and less than one quarter in rendering. Most benchmarks don’t measure real world performance. This is real world performance.

This ReactJS.org FAQ page page isn’t doing much besides hydrating the server rendered DOM and it still spends almost half the time running React code. How do you think your web app with lots of heavy interaction is going to perform?

On this React FAQ page, if we filter by JavaScript we can see that the majority of the time isn’t spent parsing the JavaScript, but by running it.

Running JavaScript is not free. Redeclaring variables, arrays, and closures on every render and throwing them away is not free. Checking dependency arrays is not free.

Let’s take a simple function.

function foo(bar) { if (bar > 0.5) { bar += 1 bar -= baz } else { bar -= 1 bar += biz } return `${bar} is the number` }

No loops, one conditional. You can benchmark it here. You can run this benchmark over and over again and I’ve never seen declaring it be more than 10% faster than running it. Many times running it is faster than declaring it. If you passed this to useCallback you would be redeclaring it on every render.

How Vue Fixed React’s Hooks

As I said in the beginning the problem isn’t with hooks themselves but in the way they are implemented in React. Hooks offer a lot of benefits with regards to composition. The problems with React’s hooks come from running the hooks on every render. Vue 3 blatantly copied React’s hooks but with a major change; the hooks only need to run once before mount.

If React’s hooks and the template logic lived in separate functions like they do in Vue 3, you wouldn’t have dependency arrays and you wouldn’t be throwing away functions and variables on every render that are only needed for initialization. In Vue 3, there are no dependency arrays to check. There is no need to keep track of the order hooks were called because the hooks are only called once. React should copy Vue just like Vue copied them.

To see more about how Vue implemented hooks, look here: https://vue-composition-api-rfc.netlify.com/