3. Memoizing a function definition with useCallback

Photo by Onur Buz on Unsplash

3. Memoizing a function definition with useCallback

Memoizing a function definition with useCallback

This blog is part of a series on understanding difference between memo, useMemo and useCallback. click here to check out more blogs.

if you jumped directly to this blog, this codesandbox contains our progress so far.

let's implement delete task feature to understand usage of usecallback. the first thing we're going to do is implement handleDelete function in app component:

// app.tsx
const handleDelete = (id: number) => {
  settodolist((prevlist: array<todo>) => {
    return prevlist.filter((task) => task.id !== id);
  });
};

then passing it to the todolist component as a prop:

// app.tsx
<todolist todolist={filteredtodolist} handledelete={handleDelete} />

also, update types.ts file and the props of the TodoList:

// types.ts
...
export type handleDelete = (id: number) => void;
// TodoList.tsx
import { Todo, handleDelete } from "./Types";
...
type Props = {
  todoList: Array<Todo>,
  handleDelete: handleDelete,
};

Next, you need to destructure handleDelete from props and pass it down to the Task component:

// TodoList.tsx
function TodoList({ todoList, handleDelete }:Props) {
  ...
  <Task key={id} id={id} task={task} handleDelete={handleDelete} />
  ...
}

In Task component, we need to update props and add button that calls handleDelete on onClick:

import { Todo, handleDelete } from "./Types";
...
type Props = Todo & { handleDelete: handleDelete };
...

function Task({ id, task, handleDelete }: Props) {
  ...
  return (
    <li>
      <span style={{ marginRight: "0.5rem" }}>{task}</span>
      <button onClick={() => handleDelete(id)}>X</button>
    </li>
  )
}
...

also, enable console.log in Task and TodoList component.

At this point, our application should look like the following and should be able to delete the task.

after adding delete button

upon clicking on X:

after deleting the task

everything looks good till now. but again we have little problem try typing something in the input, here "Complete Assignment":

writing in input after delete

on just typing "Compl" you will see set of un-necessary renders, you might think that 'even after adding HOC memo, why still these batches of renders are appearing?'. the reason for this is that we have declared handleSearch function in the App component and we're passing it from App to List and to Task component, the issue is this function is declared every time whenever component renders. so how do we fix this problem?

as you might have already figured out, using useCallback, let's see how to do that.

useCallback hook has very similar syntax to the useMemo, but the main difference is that useMemo memoizes result of the function and useCallback memoizes function definition instead.

useCallback(() => someFunctionDefinition, [dependencies]);

modify handleDelete like this:

// App.tsx
const handleDelete = useCallback((id: number) => {
  setTodoList((prevList: Array<Todo>) => {
    return prevList.filter((task: Todo) => {
      return task.id !== id;
    });
  });
}, []);

Now, our app should just fine if we write "Complete Assignment":

after adding useCallback

as expected, we don't have any extra renders of Task and List components.

Memoize function passed as an argument in effect

If you jumped directly to this section, this codesandbox contains our progress so far.

There is a special case in which you will need to use the useCallback hook. this is when you pass function as argument(dependency) in useEffect hook, for that add below code to App component:

// App.tsx
const printTodoList = () => {
  console.log("changing todolist");
};
...
useEffect(() => {
  printTodoList();
}, [todoList]);
...

since, we're listening for changes in todoList, we added that as a dependency and also remove all console.log in the app.

Now, if you check the preview for this code and delete or add a new task:

delete after adding useEffect

everything works fine. now add todoList state variable:

// App.tsx
const printTodoList = () => {
  console.log("changing todoList", todoList);
};

You'll get following warning:

missing dependency warning

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

//  App.tsx
...
useEffect(() => {
  printTodoList();
}, [todoList, printTodoList]);
...

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

another add dependency warning

the reason for this warning is useEffect is going to fire on every render, because printTodoList is going to re-declared on every render and also we're manipulating state(consoling the state), so we can use useCallback hook to fix this issue:

const printTodoList = useCallback(() => {
  console.log("changing state", todoList);
}, [todoList]);

Now, if add or delete task, it should work fine:

after adding useCallback

That's a wrap, the final code is here.that's a lot of information to grasp, so let's sum it up:

memo:

  • memoize component
  • re-memoize when props change
  • avoid re-renders

useMemo:

  • memoize calculated value
  • for computed properties
  • for heavy processes

useCallback:

  • memoize function definition to avoid redefining it on each render
  • use whenever pass function as an effect argument(dependency)
  • use it with the function passed as props

and finally remember the golden rule: do not use them until absolute necessary.

Source:

for reference, here is the code we have so far.