React Hooks Common Mistakes
Tomer Raitz
Posted on November 19, 2020
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.
- 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. -
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 likeuseLayoutEffect
). -
States. In classes, we initialized the state via
this.state = {...}
and needed to applysetState
any time we wanted to update it. With React Hooks, we can separate thestate
withuseState
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>
);
}
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>
);
}
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])
}
...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>
);
}
To understand this code better, we can break it down into its individual parts.
- We have two components:
List
, which receives props, andApp
. - App has a button when you click on it, which adds + 1 to
num
. - App also has a child component,
List
, which sends thenum
as props. - The
List
(for now), adds + 1 to theprops.num
and pushes it to thearr
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]);
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>;
}
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>;
}
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;
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;
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.
Posted on November 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.