paint-brush
Introduction to React Hooks and Why They are the Wrong Abstraction by@malerba118
711 reads
711 reads

Introduction to React Hooks and Why They are the Wrong Abstraction

by Austin MalerbaDecember 14th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

An alternative API is a bit verbose, but it is less computationally wasteful, more conceptually accurate, and it's framework agnostic. I propose an alternative API that is as capable, but with fewer caveats. Hooks are static through the lifetime of a component and should be declared on component construction instead of during the render phase. Hook's placement during a component's first render determines where the hooks must be found by React on every subsequent render. React uses lint rules and will throw errors to try to prevent developers from violating this detail of hooks. This becomes problematic when our components become bloated with lots of state and logic.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Introduction to React Hooks and Why They are the Wrong Abstraction
Austin Malerba HackerNoon profile picture

Before I get started, I'd like to express how grateful I am for all of the work that the React team has put in over the years. They've created an awesome framework that in many ways was my introduction to the modern web. They have paved the path for me to believe the ideas I'm about to present and I would not have arrived at these conclusions without their ingenuity.

In the following paragraphs, I would like to walk step by step through my observed shortcomings of hooks and propose an alternative API that is as capable, but with fewer caveats. I'll say right now that this alternative API is a bit verbose, but it is less computationally wasteful, more conceptually accurate, and it's framework agnostic 🎉.

Hooks Problem #1: Attached during render

As a general rule of design, I've found that we should always first try to disallow our users from making mistakes. Only if we're unable to prevent the user from making a mistake, should we then inform them of the mistake after they've made it.

For example, if allowing a user to enter a quantity in an input field, we could allow them to enter alphanumeric characters and then show them an error message if we find an alphabetic character in their input. However, we could provide better UX if we allowed them only to enter numeric characters into the field, which would eliminate the need to check whether they have included alphabetic characters.

React behaves quite similarly.

If we think about hooks conceptually, they are static through the lifetime of a component.

By this I mean that once declared, we cannot remove them from a component or change their position in relation to other hooks. React uses lint rules and will throw errors to try to prevent developers from violating this detail of hooks.

In this sense, React allows the developer to make mistakes and then tries to warn the user of their mistakes afterward. To see what I mean, consider the following example.

This produces an error on the second render when the counter is incremented because the component will remove the second

useState
hook.

"Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement."

The placement of our hooks during a component's first render determines where the hooks must be found by React on every subsequent render.

Given that hooks are static through the lifetime of a component, wouldn't it make more sense for us to declare them on component construction as opposed to during the render phase? If we attach hooks during the construction of a component, we no longer need to worry about enforcing the rules of hooks, because never again during the lifetime of a component would a hook be given the chance to change positions or be removed.

Unfortunately, function components were given no concept of a constructor, but let's pretend that they were. I imagine that it would look something like the following.

By attaching our hooks to the component in a constructor, we wouldn't have to worry about them changing during re-renders.

If you're thinking at this point, "you can't just move hooks to a constructor, they need to run on every render to grab the latest value", then you're totally correct!

We can't just move hooks out of the render function because we will break them. That's why we'll have to replace them with something else. But first, the second major problem of hooks.

Hooks Problem #2: Assumed state changes

We know that any time a component's state changes, React will re-render that component. This becomes problematic when our components become bloated with lots of state and logic. Say we have a component that has two unrelated pieces of state, A and B. If we update state A, our component re-renders due to the state change. Even though B has not changed, any logic that depends on it will re-run unless we wrap that logic with

useMemo
/
useCallback
.

This is wasteful because React essentially says "okay yes, recompute all these values in the render function" and then it walks back that decision and bails out on bits and pieces whenever it encounters

useMemo
or
useCallback
. However, it would make more sense if React would run only exactly what it needed to run.

Reactive Programming

Reactive programming has been around for a long time but has recently become a popular programming paradigm among UI frameworks.

The core idea of reactive programming is that variables are observable and whenever an observable's value changes, observers will be notified of this change via a callback function.

Note how the callback function passed to

observe
executes any time that we change the
count$
observable's value. You might be wondering about the
$
on the end of
count$
. This is known as Finnish Notation and simply indicates that the variable holds an observable.

In reactive programming there is also a concept of computed/derived observables that can both observe and be observed. Below is an example of a derived observable which tracks the value of another observable and applies a transform to it.

This is similar to our previous example except now we will log a doubled count.

Improving React with Reactivity

With the basics of reactive programming covered let's look at an example in React and improve upon it by making it more reactive.

Consider an app with two counters and a piece of derived state that depends on one of the counters.

Here we have logic to double the value of

countTwo
on each render, but if
useMemo
finds that
countTwo
holds the same value that it did on the previous render, then the doubled value will not be re-derived on that render.

Combining our earlier ideas, we can pull state responsibilities out of React and instead set up our state as a graph of observables in a constructor function.

The observables will notify the component whenever an observable changes so that it knows to re-render.

In the above example, the observables we create in the constructor are available in the render function via closure, which allows us to set their values in response to click events.

doubledCountTwo$
observes
countTwo$
and doubles its value only when the value of
countTwo$
changes.

Notice how we don't derive the doubled count during the render, but prior to it. Lastly, we use the

observe
function to re-render our component any time that any of the observables change.

This is an elegant solution for several reasons:

  1. State and effects are no longer the responsibility of React, but rather that of a dedicated state management library which can be used across frameworks or even without a framework.
  2. Our observables are initialized only on construction so we don't have to worry about violating the rules of hooks or re-running hook logic unnecessarily during renders.
  3. We avoid re-running derivational logic at unnecessary times by choosing to re-derive values only when their dependencies change.

With a little hacking on the React API, we can make the above code a reality.

This is actually quite similar to the way Vue 3 works with its composition API. Though the naming is different, look how strikingly similar this Vue snippet is.

If this isn't convincing enough, look how simple refs become when we introduce a constructor layer to React function components.

We actually eliminate the need for

useRef
, because instead we can declare variables in the constructor and then read/write them from anywhere through the lifetime of the component.

Perhaps cooler yet, we could easily make refs observable.

Of course, my implementation of

observable
,
derived
, and
observe
here are buggy and do not form a complete state management solution.

Not to mention these contrived examples leave out several considerations, but no worries, I have put a lot of thought in to this matter and my thoughts have culminated to a new reactive state management library, elementos!!!

Elementos

Elementos is a framework-agnostic reactive state management library with an emphasis on state composability and encapsulation. If you've enjoyed this article, I'd highly encourage you to check it out!

Here's a few links:

PS: follow me on Twitter if you haven’t! I mostly post thoughts about web dev.

Also published at https://austinmalerba.medium.com/why-react-hooks-are-the-wrong-abstraction-8a44437747c1