Understanding React Hooks - Part Two

In part-1, you have learnt what are hooks and their rules, in this blog post we will learn the difference between component life cycle methods that we use on class components and the new React effects. Even if you have read in other places that they are the same, just with a different syntax, this isn't correct.
Understanding useEffect:
When you work with useEffect
, you need to think in effects. If you want to perform the equivalent method of componentDidMount
using useEffect
, you can do the following:
useEffect(() => {
// Here you perform the side effect
}, []);
The first parameter is the callback of the effect that you want to execute, and the second parameter is the dependencies array. If you pass an empty arrray ([]) on the dependencies, the state and props will have their original initial values.
However, it is important to mention that even though this is the closest equivalent for componentDidMount
, it does not have the same behaviour. Unlike componentDidMount
and componentDidUpdate
, the function that we pass to useEffect
fires after layout and paint, during a deferred event. This normally works for many common side effects, such as setting up subscriptions and event handlers, because most types of work shouldn't block the browser from updating the screen.
However, not all effects can be deferred. For example, you would get a blink if you need to mutate the Document Object Model (DOM). This is the reason why you must fire the event synchronously before the next paint. React provides one Hook called useLayoutEffect
, which works in the exact same way as useEffect
.
Firing an effect conditionally
If you need to fire an effect conditionally, then you should add a dependency to the array of dependencies, otherwise, you will execute the effect multiple times and this may cause an infinite loop. If you pass an array of dependencies, the useEffect
Hook will ony run if one of those dependencies changes:
useEffect(() => {
// when you pass an array of dependencies the useEffect hook will only
// run
// if one of the dependencies changes
}, [dependencyA, dependencyB]);
If you understand how the React class life cycle methods works, basically,
useEffect
behaves in the same way ascomponentDidMount
,componentDidUpdate
, andcomponentWillUnmount
combined.
The effects are very important, but let's also explore some other important new Hooks, including useCallback
, useMemo
, and memo
.
Understanding useCallback, useMemo, and memo
In order to understand the difference between useCallback, useMemo and memo, we will do a to-do list example. You can create a basic application by using create-react-app template.
npx create-react-app todo
Right after that, you can remove all the extra files (App.css, App.test.css, index.css, logo.svg, reportWebVitals.js. and setupTests.js). You just need to keep the App.js file, which will contain the following code:
// Dependencies
import { useState, useEffect, useMemo, useCallback } from 'react';
// Components
import List, { Todo } from './List';
const initialTodos = [
{ id: 1, task: 'Go shopping' },
{ id: 2, task: 'Pay the electricity bill'}
];
function App() {
const [todoList, setTodoList] = useState(initialTodos);
const [task, setTask] = useState('');
useEffect(() => {
console.log('Rendering <App />');
});
const handleCreate = () => {
const newTodo = {
id: Date.now(),
task
};
// pushing the new todo the list
setTodoList([...todoList, newTodo]);
// Resetting input value
setTask('');
}
return (
<>
<input
type="text"
value={task}
onChange={(event} => setTask(event.target.value)}
/>
<button onClick={handleCreate}>Create</button>
<List todoList={todoList} />
</>
);
};
export default App;
Basically, we are defining some initial tasks and creating the todoList state, which we will pass to the list component. Then you need to create the List.js
file with the following code:
// Dependencies
import { useEffect } from 'react';
// Components
import Task from './Task';
const List = (props) => {
const { todoList } = props;
useEffect(() => {
// this effect is executed every new render
console.log('Rendering <List />');
});
return (
<ul>
{todoList.map((todo) => (
<Task key={todo.id} id={todo.id} task={todo.task} />
))}
</ul>
);
}
export default List;
As you can see, we are rendering each task of the todoList
array by using the Task
component and we pass task as a prop. I also added a useEffect
Hook to see how many renders we are performing.
Finally, we create our Task.js
file with the following code:
import { useEffect } from 'react';
const Task = (props) => {
const {id, task} = props;
useEffect(() => {
console.log('Rendering <Task />', task);
})
return (
<li>{task}</li>
);
}
export default Task;
This is how we should see the to-do list:
As you can see, when we render our to-do list, by default, we are performing two renders of the Task component, one render for List, and the other for the App component.
Now, if we try to write a new task in the input, we can see that for each letter we write, we will again see all those renders:
As you can see, by just writing 'Go', we have two new batches of renders, so we can determine that this component does not have good performance, and this is where memo can help us to improve performance. In the next part, we will see how to implement memo, useMemo, and useCallback to memoize a component, a value, and a function.