Lesser spotted React mistakes: Hooked on a feeling

by gabriel vivas|

Lesser spotted React mistakes. JavaScript | TypeScript

This series is dedicated to the small, but common pitfalls and errors you can encounter when writing React code. Whether an experienced JavaScript | TypeScript developer or starting out, the results can be surprising.

These are the kind of issues you want to catch early in your IDE before you spend hours debugging. You can copy/paste the code examples in VS Code with the SonarLint plugin if you want to see them for yourself and try to catch them before they happen to you!

Part 1: Hooked on a feeling

In this first installment of the series, we’ll look at things that can go wrong with React Hooks. Leaving you waiting for something to happen that never does, or tied in an endless loop loop loop.

You may fall into these pitfalls when you’re new to React Hooks. Although experienced devs will also raise one or both eyebrows. Let’s jump in!

The first rule of Hooks

If you know the rules of Fight Club, that won’t help you here. We need to talk about Hooks.

React introduced Hooks to enhance Functional Components. Compared to Class Components, they lacked life cycle methods among other features.

Likely the simplest example of Hooks is to store component state. Look at this example:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Let’s go over that quickly. When you invoke useState it will return an Array of two elements, the current value and a setter function to change the value. Here we are destructuring the Array with the square brackets syntax, and we are choosing the names that make sense for us. By convention, you name the setter set plus the name of our value, setCount in this case. The argument passed to the useState function is the initial value of count.

You would then use setCount to change the state to how you would like it.
Look at this other example that allows you to select your language preference. Look for a treacherous bug in there:

import React, { useState } from "react";

function ShowLanguage() {
    const [language, setLanguage] = useState("en-EN");

    setLanguage(navigator.language ?? "en-EN");

    return (
      <section>
        <h1>Your language is {language}!</h1>
        <button onClick={() => setLanguage("fr-FR")}>Je préfère le Français</button>
      </section>
    );
}

Congratulations, you have created your first infinite loop with React Hooks 🪝.

This problem was portrayed by a developer (let’s say artist) in a tweet:

https://twitter.com/marcingajda91/status/1530892067408429063/

Vibing Cat meme with a Turkish street drummer, except the drummer's face is the React logo. You also see an overlayed console error that keeps incrementing the number of uncaught errors caused by too many re-renders. An overlay text says: Me, who put the state update directly in the component's body.

As it happens, when you call the setter function of your component state, React will trigger a re-render of the component. That makes sense, after all, that’s what you would expect most of the time.

The problem here is that every time the component is trying to render, then setLanguage is called, triggering a render, then setLanguage is called, triggering a render. You get the point: calling the setter of useState at the top level of your component will produce an infinite render loop ⏳.

Going back to the code example, if you want to initialize the value of your state, pass it as the argument to useState:

import React, { useState } from "react";

function ShowLanguage() {
    const initialLanguage = navigator.language ?? "en-EN";
    const [language, setLanguage] = useState(initialLanguage);

    return (
      <section>
        <h1>Your language is {language}!</h1>
        <button onClick={() => setLanguage("fr-FR")}>Je préfère le Français</button>
      </section>
    );
}

React has actual Rules of Hooks which work well with Eslint, surprisingly this one is not included.

Fortunately, SonarLint will give you a timely error as you are introducing the bug ⏰. You can try it yourself in VSCode.

State update in an empty forest

If a tree falls in the forest, and there’s no one around to hear it, does it update your component state?

Okay, that metaphor might not work here 🌲. Let’s talk about things that don't work.

Now that you are more familiar with Hooks, here is one startling bug that can happen to you when working with a component's state. Try to find it in this code snippet:

import { useState } from "react";

function Tree() {
    const [falling, setFalling] = useState(false);

    return (
      <Forest>
        <h1>{falling ? "Fearful noise!" : "Perfect silence."}</h1>
        <button onClick={() => setFalling(falling)}>Toggle noise</button>
      </Forest>
    );
}

As it happens, the onClick handler never changes the state of the component. Or does it? That is for philosophers to debate. For what matters to us, the Tree component will always render: “Perfect silence.”

Did you see it immediately? We actually intended to invert the Boolean falling :

import { useState } from "react";

function Tree() {
    const [falling, setFalling] = useState(false);

    return (
      <Forest>
        <h1>{falling ? "Fearful noise!" : "Perfect silence."}</h1>
        <button onClick={() => setFalling(!falling)}>Toggle noise</button>
      </Forest>
    );
}

Calling the state setter with the same value will produce no perceptible change. But in reality, that is never the intention of the programmer.

React will not complain in this case. After all, technically there is nothing wrong. Right?

Unfortunately, it can happen to any of us, either by a small typo or a quick copy/paste that went wild 🪓.

As expected, SonarLint knows that you never want this philosophical paradox in your code. It will raise a warning as you type 🤖. As always, you can try it out for yourself in VSCode.

Prevent issues before they happen

As you see, there can be some non-obvious bugs with the useState hook in React.

By default, SonarLint will detect these issues and warn you as they come up, so you can fix them on the spot, without losing focus. If you want to dig deeper, SonarLint will also provide an explanation of why they happen in the first place 🤓. Sort of what we did in this article.

Next up: “Part 2: Zombie methods”

In the next installment we’ll cover issues that come up when you have dead-code methods or intrusive neighbors. Stay tuned 🐋!

If you liked this post, send us a Tweet @SonarSource or a comment in the Community. We’d love to hear about your experience.

Read more about these rules in our catalog:

S6442 React's useState hook should not be used in the render function or body of a component

S6443 React state setter function should not be called with its matching state variable