Understanding React Hooks - Part Three

Understanding React Hooks - Part Three

In part-2 you have explored react effects and in continuation to part two, we will implement memo, useMemo, and useCallback to memoize a component, a value, and a function.

Memoizing a component with memo

The memo High Order Component (HOC) is similar to PureComponent of a React class because it performs a shallow comparison of the props (meaning a superficial check), so if we try to render a component with the same props all the time, the component will render just once and will memorize. The only way to re-render the component is when a prop changes its value.

In order to fix our components to avoid the mutliple renders when we write in the input, we need to wrap our components on the memo HOC.

The first component we will fix is our List component, and you just need to effect import memo and wrap the component on export default:

import { useEffect, memo } from 'react';
...
...
export  default memo(List);

Then you need to do the same with the Task component:

import { useEffect, memo } from 'react';
...
...
export  default memo(Task);

Now, when we try to write 'Go' again in the input, let's see how may renders we get this time:

                                               

Now, we just get the first batch of renders the first time, and then, when we write 'Go', we just get two more renders of the App component, which is totally fine because the task state (input value) that we are changing is actually part of the App component.

Also, we can see how many renders we are performing when we create a new task by clicking on the Create button.

                                               

If you see, the first 16 renders are the word counting of the 'Go to the doctor' string, and then, when you click on the Create button, you should see one render of the Task component, one render of List, and one render of the App component. As you can see, we have improved performance a lot, and we are just performing the exact need that it renders.

At this point, you're probably thinking that the correct way is to always add memo to our components, or maybe you're thinking why React doesn't do this by default for us?

The reason is performance, which means it is not a good idea to add memo to all our components unless it is totally necessary, otherwise, the process of shallow comparisons and memorization will have inferior performance than if we don't use it.

I have a rule when it comes to establishing whether it is a good idea to use memo, and this rule is straightforward: just don't use it. Normally, when we have small components or basic logic, we don't need this unless you're working with large data from some API or your component needs to perform a lot of renders (normally huge lists), or when you notice that your app is going slow. Only in that case it is better to use memo.

Memoizing a value with useMemo

Let's suppose that we now want to implement a search feature in our to-do list. The first thing we need to do is to add a new state called term to the App component:

const [term, setTerm] = useState('');

Then we need to create a function called handleSearch:

const handleSearch = () => {
  setTerm(task);
}

Right before the return, we will create filterTodoList, which will filter the to-do's based on the task, and we will add a console there to see how many time it is being rendered:

const filteredTodoList = todoList.filter((todo) => {
  console.log('Filtering...')
  return todo.task.toLowerCase().includes(term.toLowerCase())
});

Finally, we need to add a new button next to the Create button that already exists:

<button onClick={handleSearch}>Search</button>

At this point, I recommend that you remove or comment console.log in the List and Task components so that we can focus on the performance of filtering:

                                                           

When you run the application again, you will see that filtering is being executed twice, and then the App component as well, and everything looks good here, but what's the problem with this? Try to write 'Go to the doctor' again in the input and let's see how many Rendering and Filtering you get:

                                                           

As you can see, for each letter you write, you will get two filtering calls and one App render and you don't need to be a genius to see that this bad performance; and not to mention that if you are working with a large data array, this will be worse, so how can we fix this issue?

The useMemo Hook is our hero in this situation, and basically, we need to move our filter inside useMemo, but first let's see the syntax

const filteredTodoList = useMemo(() => SomeProcessHere, []);

The useMemo Hook will memorize the result (value) of a function and will have some dependencies to listen to. Let's see how we can implement it:

const filteredTodoList = useMemo(() => todoList.filter((todo) => {
  console.log('Filtering...')
  return todo.task.toLowerCase().includes(term.toLowerCase())
}), []);

Now, if you write something again in the input, you will see that filtering won't be executed all the time, as was the case previously:

                                                         

This is great, but there is still one small problem. If you try to click on the Search button, it won't filter, and this is because we missed the dependencies. Actually, if you see the console warning, you will see this warning:

 

You need to add the term and todoList dependencies to the array:

const filteredTodoList = useMemo(() => todoList.filter((todo) => {
  console.log('Filtering...')
  return todo.task.toLowerCase().includes(term.toLowerCase());
}), [term, todoList]);

It should now work if you write 'Go' and click on the Search button:

                                                           

Here, we have to use the same rules that we used for memo; just don't use it until absolutely necessary.

Memoizing a function definition with useCallback

Now we will add a delete task feature to learn how useCallback works. The first thing we need to do is to create a new function called handleDelete in our App component:

const handleDelete = (taskId) => {
  const newTodoList = todoList.filter((todo) => todo.id !== taskId);
  setTodoList(newTodoList);
}

And then you need to pass this function to the List component as prop:

<List todoList={filteredTodoList} handleDelete={handleDelete} />

Then, in our List component, you need to include the handleDelete prop:

const {todoList, handleDelete} = props;

Next pass it down to the Task component:

....
<Task
  key={todo.id}
  id={todo.id}
  task={todo.task}
  handleDelete={handleDelete} // include this
/>
...

In the Task component, you need to create a button that will execute handleDelete onClick:

...
const {id, task, handleDelete} = props;
...
return (
  <li>{task} <button onClick={() => handleDelete(id)>X</button></li>
);
...

At this point, I recommend that you remove or comment console.log in the List and Task components, so we can focus on the performance of filtering. Now you should see the X button next to the task:

                                                           

If you click on the X for 'Go shopping', you should be able to remove it:

                                                           

So far, so good, right? But again we have a little issue with this implementation. If you now try to write something in the input, such as 'Go to the doctor', see what happens (image not added):

If you see, we are performing 71 renders of all the components again. At this point, you are probably thinking about, what is going on if we we have already implemented the memo HOC to memorize the components? But the problem now is that our handleDelete function is being passed in two components, from App to List and to Task, and the issue is that this function is regenerated every time we have a new re-render, in this case, every time we write something. So how do we fix this problem?

The useCallback Hook is the hero in this case and is very similar to useMemo in the syntax, but the main difference is that instead of memorizing the result value of a function, as useMemo does, it is memorizing the function defintion instead:

Our handleDelete function should be like this:

const handleDelete = useCallback((taskId) => {
  const newTodoList = todoList.filter((todo) => todo.id !== taskId);
  setTodoList(newTodoList);
}, [todoList]);

Now, it should work just fine if we write 'Go to the doctor' again. Instead of 71 renders, we just have 23, which is normal, and we are also able to delete tasks. 

As you can see, the useCallback Hook helps us to improve performance significantly. Next, we will see how to memorize a function passed as an argument in the useEffect Hook.

Memoizing function passed as an argument in effect

There is a special case where we will need to use the useCallback Hook, and this is when we pass a function as an argument in useEffect Hook, for example, in our App component. Let's create a new useEffect block:

const printTodoList = () => {
  console.log('Changing todoList');
}

useEffect(() => {
  printTodoList()
}, [todoList]);

In this case, we are listening for changes on the todoList state. If you run this code and you create or remove a task, it will work just fine (remember to remove all the other consoles first):

                                     

Everything works fine, but let's add todoList to the console:

const printTodoList = () => {
  console.log('Changing todoList', todoList);
}

But if you go to your IDE/Editor, you will get the following warning:

"React Hook useEffect has a missing dependency: 'printTodoList'. Either include it or remove the dependency array."

Basically, it is asking us to add the printTodoList function to the dependencies:

useEffect(() => {
  printTodoList()
}, [todoList, printTodoList]);

But now, after we do that, we get another warning:

"The 'printTodoList' function makes the dependencies of useEffect Hook change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'printTodoList' in its own useCallback() hook react-hooks/exhaustive-deps".

The reason why we get this warning is that we are now manipulating a state (consoling the state), which is why we need to add a useCallback Hook to this function to fix this issue:

const printTodoList = useCallback(() => {
  console.log('Changing todoList', todoList);
}, [todoList]);

Now, when we delete a task, we can see that todoList updated correctly.

At this point, this may be information overload for you, so let's have a quick recap:

memo:

  • Memorizes a component
  • Re-memorizes when props change
  • Avoids re-renders

useMemo:

  • Memoizes a calcuated value
  • For computed properties
  • For heavy process

useCallback:

  • Memorizes a function defintion to avoid redifining it on each render.
  • Use it whenever a function is passed as an effect argument.
  • Use it whenever a function is passed by props to a memorized component.

That's it. In next part, we will see how to use the useReducer Hook.