React Hooks Common Mistakes

tomeraitz

Tomer Raitz

Posted on November 19, 2020

React Hooks Common Mistakes

Alt Text
This article originally appeared at bugfender.com: react-hooks-common-mistakes.

React Hooks is a new addition to React which enables you to use state and other features of the library without having to create a class.

By 'hooking into' React's existing features, you can significantly reduce the number of concepts you have to grapple with, and you can create custom hooks to share all kinds of non-visual logic, making your code much more reusable. React Hooks are engineered to mesh with your existing codebase, so you don't need to go back and rip up the code you've already written.

This promises to be a huge step forward for the dev community, and this post will help you maximize the opportunity by flagging a number of common mistakes. I've made most (if not all) of these errors myself, so I'll be talking with the benefit of experience!

First of All, What Is The Difference Between React Hooks and Class Components?

Let's look at some of the key variations between React Hooks and the Class Components typically used to write React code.

The good news is that most of the basic concepts are still the same. However, there are some essential differences to bear in mind - and all of them are beneficial.

  1. In React Hooks, all the components are functions. So we don't need this and there's no need to deal with all the "bind" issues.
  2. Easier lifecycle. In React Hooks, we don't need to memorize the entire react lifecycle and manage it in our code. In fact most of the lifecycle can be managed from useEffect (and some less common methods like useLayoutEffect).
  3. States. In classes, we initialized the state via this.state = {...} and needed to apply setState any time we wanted to update it. With React Hooks, we can separate the state with useState and update individual parts of it.

Common Mistakes With useEffect

Ok, so now let's get into the nitty-gritty and look at some of the common mistakes that I (and other devs) have made. We'll begin by looking at useEffect.

To give this a little extra edge, try to guess the error before continuing reading. This way, you can test your React Hooks knowledge.

To kick things off, here is some basic code:

import React, {useEffect, useState} from "react";
import "./styles.css";

export default function App() {
  const [arr, setArr] = useState([]);
  useEffect(()=>{
    setArr([1])
  })
  return (
    <div className="App">
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you've hopefully spotted, the code reaches Uncaught RangeError: Maximum call stack size exceeded and cause an infinity loop.

This happens because of the missing useEffect dependency. This dependency "tells" the useEffect to run the function if the dependency is changed (like shouldComponentUpdate).

Without the dependency, useEffect will be active after any render (when we do setArr, we cause a re-render).

L*et's add an arr dependency and see what happens:*

import React, {useEffect, useState} from "react";
import "./styles.css";

export default function App() {
  const [arr, setArr] = useState([]);
  useEffect(()=>{
    setArr([1])
  },[arr])
  return (
    <div className="App">
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Despite the new addition, the code still reaches Uncaught RangeError: Maximum call stack size exceeded.

This type of error is pretty hard to understand. If we run the code in this way (and remember, this is just for explanation)...

useEffect(()=>{
    setArr(arr)
  },[arr])
}
Enter fullscreen mode Exit fullscreen mode

...we don't cause an infinity loop.

How can we explain the difference?

Actually, it's all because Arrays in JavaScript are references. So when we do setArr([1]) it's the same as arr = [1].

In this way, we change the reference any time we perform useEffect(()=>{setArr([1])},[arr]) and cause an infinity loop.

Now, let's look at a more extensive codebase:

import React, { useEffect, useState, useCallback } from "react";
import "./styles.css";

function List(props) {
  const [arr, setArr] = useState([]);
  useEffect(() => {
    setArr((previousArray) => [...previousArray, props.num + 1]);
  }, []);
  return <button>Click me</button>;
}

export default function App() {
  const [num, setNum] = useState(1);
  const addToNum = () => setNum((previousNum) => previousNum + 1);
  return (
    <div className="App">
      <button onClick={addToNum}>add num</button>
      <List num={num}></List>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

To understand this code better, we can break it down into its individual parts.

  • We have two components: List , which receives props, and App.
  • App has a button when you click on it, which adds + 1 to num.
  • App also has a child component, List , which sends the num as props.
  • The List (for now), adds + 1 to the props.num and pushes it to the arr in the first render (empty dependency).

This code works fine. The arr will be [2].  But we get a warning: React Hook useEffect has a missing dependency: props.num. Either include it or remove the dependency array.

Everything works fine, and nothing is stuck, so why are we getting this warning? 

In fact we have to thank the brilliant React engineers here, because they have already flagged this for us.

The props.num doesn't exist as a dependency. This means that any time we click add num, we render the child component without adding the new value to arr

Here's a solution:

useEffect(() => {
    setArr((previousArray) => [...previousArray, props.num + 1]);
  }, [props.num]);
Enter fullscreen mode Exit fullscreen mode

This way, the arr will change when the num changes.

But what about the List function?

Specifically, what happens if we want to give List the ability to add to arr from the button (element) as well? Well we need to do something like this:

function List(props) {
  const [arr, setArr] = useState([]);
  const addToArr = () => setArr((previousArray) => [...previousArray, props.num + 1]);
  useEffect(() => {
    addToArr();
  }, [props.num]);
  console.log(arr);
  return <button onClick={addToArr}>Add to array</button>;
}
Enter fullscreen mode Exit fullscreen mode

The code works fine (it does what we want), but now we see the warning: React Hook useEffect has a missing dependency: 'addToArr'. Either include it or remove the dependency array

In fact, if we add the addToArr to the dependency list, it will cause an infinity loop (I believe it's  self-invoked and the result of the function is different on any render. if you know the reason, please add a comment below).

The solution is to add a useCallback :

function List(props) {
  const [arr, setArr] = useState([]);
  const addToArr = useCallback(() => {
    setArr((previousArray) => [...previousArray, props.num + 1]);
  }, [props.num]);
  useEffect(() => {
    addToArr();
  }, [addToArr]);
  console.log(arr);
  return <button onClick={addToArr}>Add to array</button>;
}
Enter fullscreen mode Exit fullscreen mode

useCallback memorizes the function, and in this way we can use the addToArr dependency without a problem.

If you want to read more about this, here's good explanation from StackOverflow: about-infinite-loop-in-useeffect.

One Final Mistake...

Let's say you have an application with users and admins. There is only one API that gives you all the app data (fetchDummyData), and you need to separate it into two different states (users, admins).

Try to spot what is wrong here:

import React, { useEffect, useState } from "react";
import "./styles.css";

function App() {
  const [users, setUsers] = useState([]);
  const [admins, setAdmins] = useState([]);

  const fetchDummyData = () =>
    new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve([
          { name: "Tomer", type: "user" },
          { name: "John", type: "admin" },
          { name: "Dan", type: "user" }
        ]);
      }, 300);
    });

  const findUsers = (data) =>
setUsers(() => data.filter((row) => row.type === "user"));

  const findAdmins = (data) =>
    setAdmins(() => data.filter((row) => row.type === "admin"));

  useEffect(() => {
    const promiseData = fetchDummyData();
    promiseData.then((data) => {
      findUsers(data);
      findAdmins(data);
    });
  }, []);
  console.count("render");
  return (
    <div className="App">
      <Users users={users}></Users >
      <Admins admins={admins}></Admins >
    </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

As you probably noticed from the console.count("render"), there is something wrong with the number of renders.

According to basic logic, we need to see render: 2 the first time the component mounts, and then a re-render after useEffect. But in fact we see render : 3.

This is because any time we deploy the useState method, the component re-renders (setUsers , setAdmins).

NB: If you are using React.StrictMode in index.js, it will re-render twice. This means you will see the result of console.count in multiply 2 ("render:6"). for more information https://reactjs.org/docs/strict-mode

The solution, in this case, is to use a state like this:

function App() {
  const [allUsers, setAllUsers] = useState({ users: [], admins: [] });

  const fetchDummyData = () =>
    new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve([
          { name: "Tomer", type: "user" },
          { name: "John", type: "admin" },
          { name: "Dan", type: "user" }
        ]);
      }, 300);
    });
  const findUsers = (data) => data.filter((row) => row.type === "user");
  const findAdmins = (data) => data.filter((row) => row.type === "admin");
  useEffect(() => {
    const promiseData = fetchDummyData();
    promiseData.then((data) => {
      setAllUsers({
        users: findUsers(data),
        admins: findAdmins(data)
      });
    });
  }, []);
  console.count("render");
  return (
    <div className="App">
      <Users users={allUsers.users}></Users >
      <Admins admins={allUsers.admins}></Admins >
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This way we cause the opponent to re-render only once, because we set the state only once. If you have a very complex state, maybe the better solution is to apply useReducer.

Remember: Bugfender Can Help

Bugfender can help you to find errors in your apps. There may be some errors that you cannot see in development and only happen in production, to certain users.

Once you install Bugfender in your app, we will be notified of any problem that your app users experience.

Thank you for reading. I hope you enjoyed the tutorial and learned something new. If you have something to add, please leave a comment.

💖 💪 🙅 🚩
tomeraitz
Tomer Raitz

Posted on November 19, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related