React Hooks

Chapter 20 14 mins

Learning outcomes:

  1. What are Hooks
  2. The Hooks in React
  3. Rules of using Hooks
  4. Why were Hooks introduced

What are Hooks?

Hooks are perhaps one of the coolest features of modern-day React.

If you're a veteran React developer, and have not yet heard of Hooks, which is almost impossible, then now is the time to know about them. Hooks are the present and future of React.

Alright, now you ask "What exactly are Hooks?" Let's see them.

Hooks are functions that allow components to hook into given utilities in React.

Let's deconstruct this definition.

Starting with the first thing, Hooks are functions — normal JavaScript functions that we all know and work with all day long. But these aren't just any ordinary functions.

Hooks allow components to be hooked into given utilities provided at the dispense of developers by React, for e.g. the state utility, side-effects utility, the refs utility, and so on.

In fact, that's where the term 'Hooks' comes from. That is, a Hook allows a component to 'hook' into a given utility.

Hooks weren't always a part of React. They were introduced in React 16.8, along with stateful functional components, which we've already been using for all this time, to address the problems associated with the old way of defining components using ECMAScript-style classes. We'll go over these issues in the section below.

Due to their simplicity and close-to-functional nature, Hooks have a clear win over the old class-based style of defining components and using React's utilities within the created classes.

Hooks are essentially the reason why React has been able to ditch its approach of class-based components — they allow us to use function-based components and leverage almost all the bells and whistles of React from them.

This wasn't the case in previous versions of React where functions were solely meant to denote stateless components, that were just meant to accept in a given set of props and return some elements without any ability to work with state.

Hooks in React

React began with a relatively small number of Hooks but has now grown to host quite a decent number of them, all pretty useful in one or another kind of application.

The three most common Hooks are as follows:

  • useState() — allows a component to work with state.
  • useEffect() — allows a component to perform side effects.
  • useRef() — creates refs to be used in a component.

We've already covered all three of these Hooks in the previous chapters.

Besides these commonly used Hooks, there are other Hooks to consider:

  • useLayoutEffect() — allows a component to perform side effects before the browser repaint.
  • useReducer() — a more complex way of handling state inside a component.
  • useContext() — working with contexts in React.
  • useImperativeHandle() — allows a component to control the ref handle assigned to its parent component.
  • useCallback() — helps us reuse a particular callback function on different calls of a component.
  • useMemo() — helps us memoize a particular computationally-intensive calculation.
  • useDebugValue() — used mainly in debugging purposes along with React DevTools.

We'll cover all these Hooks — yes, that's right, all of them — in this course with practical examples of when and how to use them.

They might seem complex on the forefront but they are all superbly simple to understand and work with.

Rules of using Hooks

So far in this course, we've introduced you to three React Hooks, namely useState(), useEffect(), and useRef(). However, purposefully, we didn't discuss the rules related to using these Hooks, which are common to all Hooks in React.

These rules pertain to how we're allowed to use Hooks in React and also how we can make sure not to produce unexpected results. (You'll see what this means very shortly in this unit.)

First, let's state the rules:

  1. Hooks must be called inside components or inside functions representing custom Hooks (more on that later below).
  2. Hooks must be called in the top-level of components, not nested inside any loops, conditionals, etc.

And now, let's explain them.

Hooks must be called inside components

The first rule talks about where exactly is it allowed to call Hooks. That is, we can only call Hooks inside components or inside functions representing custom Hooks.

The latter would become clear as soon as we explore custom Hooks later on in this unit.

As for the former, calling Hooks directly inside components means that only the component function itself can contain Hook invocations; any event handlers or callbacks defined inside the component aren't valid places to call a Hook.

But why is that so? Well, it's not really that hard to see it.

React internally associates a Hook with the concrete component instance it's bound to.

When we're inside a component, React is able to determine which component instance a particular Hook invocation, like useState(), corresponds to. But when we're inside an event handler or any other kind of a callback, for e.g a callback given to setTimeout(), React has no way to tell which component instance a particular Hook invocation corresponds to.

So to prevent such kinds of cases from arising in programs, React decided to completely disallow calling Hooks from outside of components (unless we're dealing with custom Hooks, but that's a separate story).

Hooks must be called at the top-level of components

The second rule isn't really a rule, per se, but rather a sensible recommendation to follow.

React's runtime can't judge whether we ditch this second rule or not, but linter tools (that perform static analysis of our code files) can. In fact, a common practice in React toolchains is to configure linters to check for compliance with this second rule of working with Hooks.

The rule states that we must NOT nest our Hooks inside any loops, conditionals, or in general, anything that has a variable nature to it — sometimes executing, sometimes not (which is present in both loops and conditionals).

But why is this so? Again, a little bit of analysis is all that we need to get to the answer.

Internally, React associates the data for given Hook invocations based on the order of those invocations.

For instance, if we have two useState() calls, the first one is always resolved with the first set of state data held internally for the underlying component instance by React, while the second one is always resolved with the second set of state data held internally.

Now, let's suppose we introduce a third useState() call and put the second useState() behind a random conditional — sometimes executing and sometimes not.

If the conditional doesn't execute and, consequently, the second useState() doesn't execute, the third useState() call in the component will resolve with the data meant for the second call.

That is, the third useState() call turns out to be the second actual invocation and, likewise, gets resolved with the second set of state data. However, this useState() is meant to get the third set of state data.

You see where we're going.

Restating the intuition behind this rule, React relies on the order of calling Hooks in resolving those Hooks with given values (if any).

When we put Hooks behind conditionals and loops, or just about any uncertainties, they might not get called in some cases and that could really put off the harmony of the Hook-data associations meant for the whole application.

To boil it down, Hooks must be called at the top-level in a function's definition.

But then how to actually use conditions to determine when to put certain Hooks into effect?

Well, we don't need to put Hook invocations behind conditionals; instead we need to put the values provided to those Hooks behind conditionals.

(opt) Why were Hooks introduced?

Although it's not required to be able to effectively work with them, understanding why Hooks were introduced into React might get us to appreciate their importance in its current ecosystem.

And it would also help us appreciate how the growing needs and applications of a piece of software ultimately shapes how the software scales and innovates, usually putting aside old ways of doing things and coming up with new and more creative ways.

It's time to talk business.

The reasons why Hooks were introduced correspond one-to-one with the reasons why classes were increasingly becoming difficult in React userland code.

Let's see those difficulties related to classes which will eventually help us understand the reasons for introducing Hooks:

  • Difficult to reuse state logic between classes
  • Functionality related to a feature scattered across different methods
  • Hard-to-follow class conundrums

Hooks were designed to solve all these issues.

Hooks allow us to easily reuse state logic in different components, thanks to the pattern of custom Hooks (as we shall learn later on).

Moreover, Hooks focus on features rather than focusing on class methods which otherwise contain broken chunks of those features. In that way, we can build complete features in isolation using Hooks, leading to much more intuitive coding and much better testability.

Furthermore, Hooks allow us to work with normal, basic functions which don't carry the intricacies of class inheritance, this bindings, and long boilerplate codes commonly associated with setting up classes.

To boil it all down, Hooks are the X factor of modern-day React. Take out Hooks from React, and it'll clearly not remain as intuitive, flexible, and scalable as it is today.

As simple as that.

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage