Все мы знаем, что с появлением React 16.8, в нашу жизнь пришли хуки. Действительно, они упрощают процесс написания кода, используя функциональные компоненты в сравнении с классами, но некоторые хуки вызывают много вопросов у разработчиком. В данной статье мы рассмотрим на примерах, когда же нужно использовать useCallback, а когда можно обойтись без него.

Мы знаем из документации, что useCallback возвращает мемоизированную версию коллбэка. Простыми словами возвращает одну и туже ссылку на функцию, до тех пор, пока не изменится одна из зависимостей.

const memoizedCallback = useCallback(
 () => {
    doSomething(a, b);
  },
  [a, b],
);

Давайте рассмотрим пример:

У нас имеется страница с инпутом, списком отображения элементов и кнопкой, которая добавляет в список введенный элемент. При клике на сам элемент списка, он будет удаляться. Дефолтный список возьмём из 5 элементов, который мы будем редактировать.

const listOfCities = ['Beijing','Tokyo','Kinshasa','Moscow','Jakarta'];

const Page = () => {
  const [name, setName] = useState("");
  const [list, setList] = useState(listOfCities);

  const handleClick = () => {
    setList([...list, name]);
    setName("");
  };

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setName(event.target.value);
  };

  const handleRemoveClick = (item: string) => {
    const filteredList = list.filter((listItem) => listItem !== item);

    setList(filteredList);
  };

  console.log("Page render");

  return (
    <div>
      <input type="text" value={name} onChange={handleChange} />
      <button onClick={handleClick}>Add</button>
      <CitiesList list={list} onRemoveClick={handleRemoveClick} />
    </div>
  );
};

И компоненты отображения списка

export const CitiesList = ({list, onRemoveClick}) => {
  console.log("List render");

  return list.map((item) => {
    return <City key={item} city={item} onRemoveClick={onRemoveClick} />;
  });
};

export const City = ({city, onRemoveClick}) => {
  const handleCityClick = () => onRemoveClick(city);

  console.log("Element render");

  return <div onClick={handleCityClick}>{city}</div>;
};

Итак, какие же есть сейчас проблемы с перформансом. Давайте запустим и посмотрим что у нас получилось, и сразу откроем консоль разработчика.

Рендер компонентов
Рендер компонентов
Консоль разработчика
Консоль разработчика

Кажется все логично, произошел рендер страницы, компонента списка и его 5 элементов.

Начинаем вводить в инпут символы и видим, что на каждый введенный символ, происходит аналогичный рендер во всех компонентах. Изменения значения инпут (state name), вызывает рендер во всех дочерних компонентах.

У нас есть несколько callback функции, которые теоретически можно обернуть в useCallback. Разберем их по отдельности.

  • Обернув handleClick, handleChange или handleCityClick в useCallback, увы это никак не улучшит перфоманс, ведь useCallback это тоже функция, которая при каждом рендер, будет заново сравнивать зависимости и возвращать новую или старую ссылку на функцию.

  • Обернув handleRemoveClick в useCallback, это уменьшит количество ререндеров при условии что мы обернем СitiesList в React.memo, подробнее о нем можно посмотреть в документации.

const handleRemoveClick = useCallback(
  (item: string) => {
    const filteredList = list.filter((listItem) => listItem !== item);

    setList(filteredList);
  },
  [list]
);
const CitiesList = React.memo(({ list, onRemoveClick }) => {
  console.log("List render");

  return list.map((item) => {
    return <City key={item} item={item} onRemoveClick={onRemoveClick} />;
  });
});

Теперь при вводе символов в инпуте, компонент СitiesList не перерендеривается. На список из 5 элементов улучшение перформанса будет не столь большим, но наглядно показывает как его можно будет улучшить в случае необходимости.

В данном примере можно было вынести инпут с кнопкой и его state в отдельный компонент, тогда необходимость использования useCallback уже была бы не актуальной, поэтому не делайте оптимизаций производительности до тех пор, пока это действительно не потребуется.


Какие из этого можно сделать вывод

Встроенный хук useCallback нужная и полезная вещь, которая помогает улучшить перформанс. Но её применение не всегда актуально. Используйте его для функций, которые передаются компонентам с большими затратами памяти для отображения. Делайте оптимизацию производительности после написания и рефакторинга кода.

Надеюсь, эта статья была вам полезной. Всем пока :)

Комментарии (11)


  1. romanere
    22.11.2021 01:47
    +2

    Спасибо за демонстрацию примера, но переменные в коде написаны на русских и английских символах. Если кто-то захочет скопировать и протестировать, у него ничего не выйдет.

    Рабочий код без русских символов
    import React, { useState, useCallback } from 'react';
    
    function Page() {
      const listOfCities = ['Beijing', 'Tokyo', 'Kinshasa', 'Moscow', 'Jakarta'];
    
      const [name, setName] = useState('');
      const [list, setList] = useState(listOfCities);
    
      const handleClick = () => {
        setList([...list, name]);
        setName('');
      };
    
      const handleChange = (event) => {
        setName(event.target.value);
      };
    
      const handleRemoveClick = useCallback(
        (item) => {
          const filteredList = list.filter((listItem) => listItem !== item);
    
          setList(filteredList);
        },
        [list]
      );
    
      console.log('Page render');
    
      return (
        <div>
          <input type="text" value={name} onChange={handleChange} />
          <button onClick={handleClick}>Add</button>
          <CitiesList list={list} onRemoveClick={handleRemoveClick} />
        </div>
      );
    }
    
    export const CitiesList = React.memo(({ list, onRemoveClick }) => {
      console.log('List render');
    
      return list.map((item) => {
        return <City key={item} city={item} onRemoveClick={onRemoveClick} />;
      });
    });
    
    export const City = ({ city, onRemoveClick }) => {
      const handleCityClick = () => onRemoveClick(city);
    
      console.log('City render');
    
      return <div onClick={handleCityClick}>{city}</div>;
    };
    
    export default Page;


    1. Roman9131 Автор
      22.11.2021 10:38

      @romanere Спасибо за внимательность, поправил переменные


  1. Pavel1114
    22.11.2021 08:00
    +1

    Полезное видео по теме от АйТи-Синяка www.youtube.com/watch?v=2Wp7QPTkpms


  1. habrjeka
    22.11.2021 10:23
    +2

    Проблема: производительность - рендер Page и всех его дочерних компонентов каждый раз при вводе в input.

    Причина: вызывается setState в handleChange при onChange input элемента

    Решение: убираем состояние useState() для name;

    без всяких useCallback
    
    import React, { useState } from "react";
    
    function Page() {
      const listOfCities = ["Beijing", "Tokyo", "Kinshasa", "Moscow", "Jakarta"];
    
      const [list, setList] = useState(listOfCities);
      let newItem = "";
      const handleClick = () => {
        setList([...list, newItem]);
        newItem = "";
      };
    
      const handleChange = (event) => {
        newItem = event.target.value;
      };
    
      const handleRemoveClick = (item) => {
        const filteredList = list.filter((listItem) => listItem !== item);
    
        setList(filteredList);
      };
    
      console.log("Page render");
    
      return (
        <div>
          <input type="text" onChange={handleChange} />
          <button onClick={handleClick}>Add</button>
          <CitiesList list={list} onRemoveClick={handleRemoveClick} />
        </div>
      );
    }
    
    export const CitiesList = ({ list, onRemoveClick }) => {
      console.log("List render");
    
      return list.map((item) => {
        return <City key={item} city={item} onRemoveClick={onRemoveClick} />;
      });
    };
    
    export const City = ({ city, onRemoveClick }) => {
      const handleCityClick = () => onRemoveClick(city);
    
      console.log("City render");
    
      return <div onClick={handleCityClick}>{city}</div>;
    };
    
    export default Page;

    Пример в статье подобран неудачно ...


    1. kahi4
      22.11.2021 11:38
      +2

      А теперь вбейте инпут, удалите город из списка, а потом нажмите add.

      Но согласен, что пример не самый удачный


      1. habrjeka
        22.11.2021 11:42

        Шах и мат :)


        1. Roman9131 Автор
          22.11.2021 23:10
          +1

          @habrjeka Спасибо за комментарии, как написано в конце статьи, действительно, данную проблему производительности можно решить путем вынесением поля ввода вместе с его state в отдельный компонент или тем методом который был предложен выше в комментарии. Моя идея была все таки не предложить идеальное решение этой задачи, а показать как можно было бы ее решить при помощи useCallback


    1. mobilz
      23.11.2021 03:26

      о господи, никогда так не делайте. при любом чихе вы будете обнулять свою переменную и контролировать это никак не получится. т.е. обновился родительский компонент -- обнулили переменную, даже если вы набирали текст в моменте.

      ну и не используйте let, забудьте на годик про let и var, используйте const


      1. habrjeka
        23.11.2021 08:13

        Вы на 100% правы.


  1. strannik_k
    23.11.2021 18:30

    Еще можно в некоторых случаях выносить функции вне компонента, а не помещать их в useCallback. Тогда будет не нужно беспокоиться о лишних перерисовках. К тому же, за счет вынесения кода, большие компоненты станут гораздо читабельнее.


  1. skeevy
    01.12.2021 18:38

    const [list, setList] = useState(listOfCities);
    const handleClick = () => {
      setList([...list, newItem]);
    };

    я надеюсь, в проде вы так не делаете, как написали на примере. Разворачивать массив еще раз, да еще и по результирующей переменной вместо использования предыдущего состояния может вам очень больших проблем подкинуть