Use React Hooks Correctly with These Two Rules

Chris Achard - Aug 14 '19 - - Dev Community

Hooks were introduced at React Conf 2018, and have been steadily gaining popularity as an easy way to introduce state to functional react components.

It's important to note that you don't have to use hooks if you don't want to - but if you do, make sure you follow these two rules of hooks! Otherwise - bad things might happen.

Why Hooks Were Created

First, let's take a look at the problem that hooks are trying to solve.

Here's a React class component with a single bit of state called count, that counts up when the button is clicked:

// OLD WAY: Class Components

class Counter extends React.Component {

  state = {
    count: 0
  }

  render() {
    return (
      <div>
        <button 
          onClick={() => 
            this.setState({ count: this.state.count + 1 })
          }
        >
          +1
        </button>
        <p>Count: {this.state.count}</p>
      </div>
    )
  }
}

That works just fine, but there are a few parts that can easily create bugs.

Problem 1: What is the meaning of this?

One of the most confusing parts of JavaScript for new developers is that the this keyword changes meaning based on the context. This includes the very important, but (seemingly) arbitrary way that you define your functions.

For example, this function:

// probably not what you want
function onClick() {
  this.setState({ this.state.count: count + 1 })
}

doesn't bind this to the class component - and so probably won't behave like you want! Instead, you have to remember to either bind that function to the class - or use an arrow function:

// probably what you want
const onClick = () => {
  this.setState({ this.state.count: count + 1 })
}

Until you get a really good feeling for what the meaning of this is in various parts of a class component, it can cause subtle, and really confusing bugs.

Hooks simplify that by removing the need to track the meaning of this altogether. This is because there is no class that you have to reference (since everything is functional components with hooks).

Problem 2: Using this.state... to access data and calling this.setState to change it

The fact that state lives on the class component means that anytime you want to access a value in state, you have to preface it with this.state. This can be confusing to beginners - but also can bite experience programmers.

As a testament to how annoying it can be - when creating the demo for this article, I originally typed the following onClick function:

// Can you spot the bug?
...
  this.setState({ count: count + 1 })
...

Do you see the bug? Yep; I forgot to say this.state.count: instead of just count: in the setState call. That didn't cause a render error or anything - but it just didn't work; and it took me some debugging to figure out what went wrong... annoying!

Hooks simplify that by removing the concept of class state, and just giving access to the values and set functions directly. No more this.state!

Problem 3: People are using functional components more and more

Since functional components are 'just functions', and are generally easier to type and to reason about - more and more people are defaulting to functional components over class components.

The problem is, that as soon as you want to add state to a functional component - you have to convert it to a class component, or bring in a more complicated library like Redux.

Hooks simplify that by giving you a way to add state to functional components directly with the useState hook.

Then - with the useEffect hook, you can replicate the lifecycle methods of class components, and suddenly - you don't need classes anymore!

So you can just keep using your functional components everywhere now.

How Hooks Work

Ok - let's take a look at the same Counter example; but this time with hooks:

// NEW WAY: Hooks
import React, { useState } from "react";
...
const Counter = () => {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
      <p>Count: {count}</p>
    </div>
  )
}

Instead of setting initial state with state = {}, we use the useState hook to define a new bit of state that we call count, which defaults to 0.

Importantly (for the rules later) - React stores this value internally in an array of all the values created with hooks in this function. We only have one here; but let's add another to see what that looks like:

// Multiple states

const Counter = () => {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('Chris')
  ...
}

So with two useState calls above, we know that React is storing an array of two values.

OK! Now we have all the information we need in order to understand the rules of hooks.

The Rules of Hooks

Rule 1: Call hooks unconditionally at the top level of your component

Because hooks are stored in an array, the order that they get called matters. Since React components are re-rendered each time data changes, that means that the exact same hooks must be called in the exact same order on every single render.

That means if you added an if statement in there:

// DON'T DO THIS!
...
if(myBool) {
  const [count, setCount] = useState(0)
}
const [name, setName] = useState('Chris')
...

The count state would sometimes be created - and sometimes wouldn't be. But React can't track that internally - so it would mess up the value of all the state stored in the array for that component.

Also - don't put hooks inside of loops (or any other control function):

// ALSO DON'T DO THIS!
...
for(my conditions) {
  ...useState...
  ...useEffect...
}
...

... because if your conditions are based on variables that change, then you'll have hooks run in different orders from render to render.

Rule 2: Only call hooks in React functions or custom hooks

This rule exists for the same reason at rule #1, but is slightly different.

For example, if we created a helper function called doSomething:

// DON'T DO THIS

function doSomething() {
  const [count, setCount] = useState(0)
  ... do something with count ...
}

Then you (or another developer) may not realize that that doSomething function actually calls a hook - and may be tempted to call doSomething out of order:

// MISUSE of doSomething

const Counter = () => {
  return <button onClick={doSomething}>Do it!</button>
}

Which breaks the hooks!

It breaks them because React internally can't keep track of hooks that run out of order (same as rule #1) - so only use hooks at the top of react components, or in custom hooks that you create.

Hooks Aren't Scary

Hooks solve a few common problems that developers have had with React class components. You don't have to use them (so don't go out and replace a bunch of your code 'just because') - but if you do, then follow the rules:

  1. Call hooks unconditionally at the top level of your component
  2. Only call hooks in React functions or custom hooks

And that's it!

Once you know that React keeps hook values in arrays, then it makes sense: don't change the order of the hooks that are called, or else React can't keep track of what's what!

This post originally posted on: https://chrisachard.com/use-react-hooks-correctly-with-these-two-rules

. . . . . . . . . . . . . . . . . . . . . . . .