useCallback

useCallback - хук React, який дозволяє кешувати визначення функції між повторними рендерингами.

const cachedFn = useCallback(fn, dependencies)

Довідник

useCallback(fn, dependencies)

Викличте useCallback на верхньому рівні вашого компонента, щоб кешувати визначення функції між повторними рендерами:

import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

Дивіться більше прикладів нижче.

Параметри

  • fn: Значення функції, яке ви хочете кешувати. Вона може приймати будь-які аргументи і повертати будь-які значення. React поверне (не викличе!) вашу функцію під час першого рендерингу. При наступних рендерингах React поверне вам ту саму функцію, якщо залежності не змінилися з моменту останнього рендерингу. В іншому випадку, він надасть вам функцію, яку ви передали під час поточного рендерингу, і збереже її на випадок, якщо вона може бути використана пізніше. React не буде викликати вашу функцію. Функція повертається вам, щоб ви могли вирішити, коли і чи викликати її.

  • залежності: список усіх реактивних значень, на які посилаються всередині коду fn. Реактивні значення включають пропси, стан і всі змінні та функції, оголошені безпосередньо у тілі вашого компонента. Якщо ваш лінтер налаштований на React, він буде перевіряти, що кожне реактивне значення правильно вказане як залежність. Список залежностей повинен мати постійну кількість елементів і бути записаний в лінію як [dep1, dep2, dep3]. React порівняє кожну залежність з попереднім значенням, використовуючи алгоритм порівняння Object.is.

Повернення

При початковому рендерингу useCallback повертає передану вами функцію fn.

Під час наступних рендерингів він або повертатиме вже збережену функцію fn з останнього рендерингу (якщо залежності не змінилися), або повертатиме функцію fn, яку ви передали під час цього рендерингу.

Застереження

  • useCallback є хуком, тому ви можете викликати його тільки на верхньому рівні вашого компонента або ваших власних хуків. Ви не можете викликати його всередині циклів або умов. Якщо вам це потрібно, витягніть новий компонент і перемістіть стан до нього.
  • React не викидатиме кешовану функцію, якщо для цього немає конкретної причини. Наприклад, під час розробки React викидає кеш, коли ви редагуєте файл вашого компонента. Як в розробці, так і у виробництві, React викидає кеш, якщо ваш компонент призупиняється під час початкового монтування. У майбутньому React може додати більше можливостей, які використовують переваги відкидання кешу - наприклад, якщо в майбутньому React додасть вбудовану підтримку віртуалізованих списків, то має сенс відкидати кеш для елементів, які прокручуються за межами області перегляду віртуалізованої таблиці. Це буде добре, якщо ви покладаєтесь на useCallback виключно як на оптимізацію продуктивності. В іншому випадку, змінна стану або посилання можуть бути більш доречними.

Використання

Пропуск повторного рендерингу компонентів

Під час оптимізації продуктивності рендерингу іноді потрібно кешувати функції, які ви передаєте дочірнім компонентам. Давайте спочатку розглянемо синтаксис, як це зробити, а потім подивимося, в яких випадках це може бути корисно.

Щоб кешувати функцію між рендерингами вашого компонента, обгорніть її визначення у useCallback Хук:

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);
  // ...

Вам потрібно передати дві речі до useCallback:

  1. Визначення функції, яку потрібно кешувати між повторними відображеннями.
  2. Список залежностей CodeStep> включаючи кожне значення з вашого компонента, що використовується у вашій функції.</li></ol> <p>При початковому рендерингу функція <CodeStep data-step="3">повернула </CodeStep> ви отримаєте з <code>useCallback буде функцією, яку ви передали.

    На наступних рендерингах React порівнює залежності та ), useCallback поверне ту саму функцію, що й раніше. Інакше useCallback поверне функцію, яку ви передали у this render.

    Іншими словами, useCallback кешує функцію між повторними рендерингами, поки не зміняться її залежності.

    Давайте розглянемо приклад, щоб побачити, коли це може бути корисним.

    Скажімо, ви передаєте функцію handleSubmit з ProductPage до компонента ShippingForm:

    function ProductPage({ productId, referrer, theme }) {
      // ...
      return (
        <div className={theme}>
          <ShippingForm onSubmit={handleSubmit} />
        </div>
      );

    Ви помітили, що перемикання пропсу theme на мить призупиняє роботу програми, але якщо ви видалите <ShippingForm /> з вашого JSX, то програма працює швидко. Це говорить про те, що варто спробувати оптимізувати компонент ShippingForm.

    За замовчуванням, коли компонент перерендериться, React рекурсивно перерендерить усі його дочірні елементи. Ось чому, коли ProductPage перерендериться з іншою темою, ShippingForm компонент також перерендериться. Це добре для компонентів, які не потребують багато обчислень для повторного рендерингу. Але якщо ви переконалися, що повторний рендер повільний, ви можете вказати ShippingForm пропустити повторний рендер, якщо його пропси такі самі, як і під час останнього рендеру, обернувши його у memo:

    import { memo } from 'react';
    
    const ShippingForm = memo(function ShippingForm({ onSubmit }) {
      // ...
    });

    З цією зміною ShippingForm пропустить повторний рендеринг, якщо всі його пропси будуть такими самими , як і під час останнього рендерингу. Ось де кешування функції стає важливим! Припустимо, що ви обчислили handleSubmit без useCallback:

    function ProductPage({ productId, referrer, theme }) {
      // Every time the theme changes, this will be a different function...
      function handleSubmit(orderDetails) {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }
      
      return (
        <div className={theme}>
          {/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
          <ShippingForm onSubmit={handleSubmit} />
        </div>
      );
    }

    У JavaScript function () {} або () => {} завжди створює іншу функцію, подібно до того, як літерал об'єкта {} завжди створює новий об'єкт. Зазвичай це не було б проблемою, але це означає, що пропси ShippingForm ніколи не будуть однаковими, і ваша memo оптимізація не працюватиме. Ось де useCallback стає у нагоді:

    function ProductPage({ productId, referrer, theme }) {
      // Tell React to cache your function between re-renders...
      const handleSubmit = useCallback((orderDetails) => {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }, [productId, referrer]); // ...so as long as these dependencies don't change...
    
      return (
        <div className={theme}>
          {/* ...ShippingForm will receive the same props and can skip re-rendering */}
          <ShippingForm onSubmit={handleSubmit} />
        </div>
      );
    }

    Загорнувши handleSubmit у useCallback, ви гарантуєте, що це буде та сама функція між рендерами (доки не зміняться залежності). Вам не потрібно обгортати функцію у , якщо тільки ви не робите цього з якоїсь конкретної причини. У цьому прикладі причиною є те, що ви передаєте її компоненту, обгорнутому у memo,, і це дозволяє йому пропустити повторний рендеринг. Є й інші причини, за яких вам може знадобитися useCallback, які описано далі на цій сторінці.

    Вам слід покладатися лише на useCallback як на оптимізацію продуктивності. Якщо ваш код не працює без нього, спочатку знайдіть основну проблему і виправте її. Після цього ви можете додати useCallback назад.

    Ви часто бачите useMemo поряд з useCallback. Вони обидва корисні, коли ви намагаєтеся оптимізувати дочірній компонент. Вони дозволяють вам запам'ятовувати (або, іншими словами, кешувати) щось, що ви передаєте далі:

    import { useMemo, useCallback } from 'react';
    
    function ProductPage({ productId, referrer }) {
      const product = useData('/product/' + productId);
    
      const requirements = useMemo(() => { // Calls your function and caches its result
        return computeRequirements(product);
      }, [product]);
    
      const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }, [productId, referrer]);
    
      return (
        <div className={theme}>
          <ShippingForm requirements={requirements} onSubmit={handleSubmit} />
        </div>
      );
    }

    Різниця в тому, щовони дозволяють вам кешувати:

    • useMemo кешує результат виклику вашої функції. У цьому прикладі він кешує результат виклику computeRequirements(product), щоб він не змінювався, якщо не змінився продукт. Це дозволяє передавати об'єкт requirements вниз без зайвого повторного рендерингу ShippingForm. За необхідності React викличе функцію, яку ви передали під час рендерингу, щоб обчислити результат.
    • useCallback кешує саму функцію. На відміну від useMemo, він не викликає надану вами функцію. Натомість він кешує надану вами функцію так, що handleSubmit сам не змінюється, доки не зміниться productId або referrer. Це дозволяє вам передавати функцію handleSubmit вниз без зайвого повторного рендерингу ShippingForm. Ваш код не буде виконано, доки користувач не надішле форму.

    Якщо ви вже знайомі з useMemo, можливо, вам буде корисно думати про useCallback так:

    // Simplified implementation (inside React)
    function useCallback(fn, dependencies) {
      return useMemo(() => fn, dependencies);
    }

    Детальніше про різницю між useMemo та useCallback.

    Чи потрібно скрізь додавати useCallback?

    Якщо ваша програма подібна до цього сайту, і більшість взаємодій є грубими (наприклад, заміна сторінки або цілого розділу), запам'ятовування зазвичай не потрібне. З іншого боку, якщо ваша програма більше схожа на редактор малюнків, і більшість взаємодій є деталізованими (наприклад, переміщення фігур), то запам'ятовування може виявитися дуже корисним.

    Кешування функції за допомогою useCallback має сенс лише у кількох випадках:

    • Ви передаєте його як проп до компонента, обгорнутого у memo. Ви хочете пропустити повторний рендер, якщо значення не змінилося. Запам'ятовування дозволяє перерендерити компонент лише у разі зміни залежностей.
    • Функція, яку ви передаєте, пізніше використовується як залежність деякого хука. Наприклад, від неї залежить інша функція, обгорнута в useCallback, або ви залежите від цієї функції з useEffect.

    В інших випадках немає ніякої користі від обгортання функції в useCallback. Істотної шкоди від цього також немає, тому деякі команди вирішують не думати про окремі випадки, а запам'ятовувати якомога більше. Недоліком цього є те, що код стає менш читабельним. Крім того, не всяке запам'ятовування є ефективним: одного значення, яке є "завжди новим", достатньо, щоб порушити запам'ятовування для цілого компонента.

    Зверніть увагу, що useCallback не заважає створювати функцію. Ви завжди створюєте функцію (і це добре!), але React ігнорує це і повертає вам кешовану функцію, якщо нічого не змінилося.

    На практиці, ви можете зробити багато запам'ятовування непотрібним, дотримуючись кількох принципів:

    1. Коли компонент візуально обгортає інші компоненти, дозвольте йому приймати JSX як дочірні. Тоді, якщо компонент-обгортка оновлює власний стан, React знатиме, що його дочірні компоненти не потребують повторного рендерингуь.
    2. Надавайте перевагу локальному стану і не піднімайте стан вище , ніж це необхідно. Не зберігайте перехідні стани, як-от форми та елементи, наведені на вершину дерева або у глобальній бібліотеці станів.
    3. Зберігайте логіку рендерингу чистою. Якщо повторний рендеринг компонента викликає проблему або створює помітний візуальний артефакт, це вада у вашому компоненті! Виправте ваду замість того, щоб додавати запам'ятовування.
    4. Уникайте непотрібних ефектів, які оновлюють стан. Більшість проблем з продуктивністю у React-додатках спричинені ланцюжками оновлень від ефектів, які змушують ваші компоненти рендерити знову і знову.
    5. Спробуйте видалити непотрібні залежності з ефектів.Наприклад, замість запам'ятовування часто простіше перемістити якийсь об'єкт або функцію всередину ефекту або за межі компонента.

    Якщо певна взаємодія все ще гальмує, використайте профайлер React Developer Tools, щоб побачити, які компоненти отримують найбільшу користь від запам'ятовування, і додайте запам'ятовування там, де це необхідно. Ці принципи полегшують налагодження та розуміння ваших компонентів, тому варто дотримуватися їх у будь-якому випадку. У довгостроковій перспективі ми досліджуємо можливість автоматичного використання гранульованої пам'яті, щоб вирішити цю проблему раз і назавжди.

    Пропуск повторного рендерингу з useCallback та memo

    У цьому прикладі ShippingFormкомпонент штучно сповільнено, щоб ви могли побачити, що відбувається, коли React-компонент, який ви рендерите, дійсно повільний. Спробуйте збільшити лічильник і перемкнути тему.

    Інкрементування лічильника відчувається повільним, оскільки воно змушує сповільнений ShippingForm рендерити повторно. Це очікувано, оскільки лічильник змінився, і вам потрібно відобразити новий вибір користувача на екрані.

    Далі спробуйте переключити тему. Завдяки useCallback таʼ memo це працює швидко, незважаючи на штучне сповільнення! ShippingForm пропустив повторний рендеринг, оскільки функція handleSubmit не змінилася. Функція handleSubmit не змінилася, оскільки productId та referrer (ваші залежності useCallback) не змінилися з часу останнього рендеру.

    import { useState } from 'react';
    import ProductPage from './ProductPage.js';
    
    export default function App() {
      const [isDark, setIsDark] = useState(false);
      return (
        <>
          <label>
            <input
              type="checkbox"
              checked={isDark}
              onChange={e => setIsDark(e.target.checked)}
            />
            Dark mode
          </label>
          <hr />
          <ProductPage
            referrerId="wizard_of_oz"
            productId={123}
            theme={isDark ? 'dark' : 'light'}
          />
        </>
      );
    }
    import { useCallback } from 'react';
    import ShippingForm from './ShippingForm.js';
    
    export default function ProductPage({ productId, referrer, theme }) {
      const handleSubmit = useCallback((orderDetails) => {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }, [productId, referrer]);
    
      return (
        <div className={theme}>
          <ShippingForm onSubmit={handleSubmit} />
        </div>
      );
    }
    
    function post(url, data) {
      // Imagine this sends a request...
      console.log('POST /' + url);
      console.log(data);
    }
    import { memo, useState } from 'react';
    
    const ShippingForm = memo(function ShippingForm({ onSubmit }) {
      const [count, setCount] = useState(1);
    
      console.log('[ARTIFICIALLY SLOW] Rendering <ShippingForm />');
      let startTime = performance.now();
      while (performance.now() - startTime < 500) {
        // Do nothing for 500 ms to emulate extremely slow code
      }
    
      function handleSubmit(e) {
        e.preventDefault();
        const formData = new FormData(e.target);
        const orderDetails = {
          ...Object.fromEntries(formData),
          count
        };
        onSubmit(orderDetails);
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <p><b>Note: <code>ShippingForm is artificially slowed down!

    ); }); export default ShippingForm;
    label {
      display: block; margin-top: 10px;
    }
    
    input {
      margin-left: 5px;
    }
    
    button[type="button"] {
      margin: 5px;
    }
    
    .dark {
      background-color: black;
      color: white;
    }
    
    .light {
      background-color: white;
      color: black;
    }

    Завжди повторний рендеринг компонента

    У цьому прикладі реалізація ShippingForm також штучно сповільнена, щоб ви могли побачити, що відбувається, коли якийсь React-компонент, який ви рендерите, дійсно працює повільно. Спробуйте збільшити лічильник і перемкнути тему.

    На відміну від попереднього прикладу, перемикання теми тепер також відбувається повільно! Це пов'язано з тим, що у цій версії немає виклику useCallback, тому handleSubmit завжди є новою функцією, і сповільнений ShippingForm компонент не може пропустити повторний рендеринг.

    import { useState } from 'react';
    import ProductPage from './ProductPage.js';
    
    export default function App() {
      const [isDark, setIsDark] = useState(false);
      return (
        <>
          <label>
            <input
              type="checkbox"
              checked={isDark}
              onChange={e => setIsDark(e.target.checked)}
            />
            Dark mode
          </label>
          <hr />
          <ProductPage
            referrerId="wizard_of_oz"
            productId={123}
            theme={isDark ? 'dark' : 'light'}
          />
        </>
      );
    }
    import ShippingForm from './ShippingForm.js';
    
    export default function ProductPage({ productId, referrer, theme }) {
      function handleSubmit(orderDetails) {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }
    
      return (
        <div className={theme}>
          <ShippingForm onSubmit={handleSubmit} />
        </div>
      );
    }
    
    function post(url, data) {
      // Imagine this sends a request...
      console.log('POST /' + url);
      console.log(data);
    }
    import { memo, useState } from 'react';
    
    const ShippingForm = memo(function ShippingForm({ onSubmit }) {
      const [count, setCount] = useState(1);
    
      console.log('[ARTIFICIALLY SLOW] Rendering <ShippingForm />');
      let startTime = performance.now();
      while (performance.now() - startTime < 500) {
        // Do nothing for 500 ms to emulate extremely slow code
      }
    
      function handleSubmit(e) {
        e.preventDefault();
        const formData = new FormData(e.target);
        const orderDetails = {
          ...Object.fromEntries(formData),
          count
        };
        onSubmit(orderDetails);
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <p><b>Note: <code>ShippingForm is artificially slowed down!

    ); }); export default ShippingForm;
    label {
      display: block; margin-top: 10px;
    }
    
    input {
      margin-left: 5px;
    }
    
    button[type="button"] {
      margin: 5px;
    }
    
    .dark {
      background-color: black;
      color: white;
    }
    
    .light {
      background-color: white;
      color: black;
    }

    Втім, ось той самий код з вилученим штучним сповільненням. Відсутність useCallback відчутна чи ні?

    import { useState } from 'react';
    import ProductPage from './ProductPage.js';
    
    export default function App() {
      const [isDark, setIsDark] = useState(false);
      return (
        <>
          <label>
            <input
              type="checkbox"
              checked={isDark}
              onChange={e => setIsDark(e.target.checked)}
            />
            Dark mode
          </label>
          <hr />
          <ProductPage
            referrerId="wizard_of_oz"
            productId={123}
            theme={isDark ? 'dark' : 'light'}
          />
        </>
      );
    }
    import ShippingForm from './ShippingForm.js';
    
    export default function ProductPage({ productId, referrer, theme }) {
      function handleSubmit(orderDetails) {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }
    
      return (
        <div className={theme}>
          <ShippingForm onSubmit={handleSubmit} />
        </div>
      );
    }
    
    function post(url, data) {
      // Imagine this sends a request...
      console.log('POST /' + url);
      console.log(data);
    }
    import { memo, useState } from 'react';
    
    const ShippingForm = memo(function ShippingForm({ onSubmit }) {
      const [count, setCount] = useState(1);
    
      console.log('Rendering <ShippingForm />');
    
      function handleSubmit(e) {
        e.preventDefault();
        const formData = new FormData(e.target);
        const orderDetails = {
          ...Object.fromEntries(formData),
          count
        };
        onSubmit(orderDetails);
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <label>
            Number of items:
            <button type="button" onClick={() => setCount(count - 1)}>–</button>
            {count}
            <button type="button" onClick={() => setCount(count + 1)}>+</button>
          </label>
          <label>
            Street:
            <input name="street" />
          </label>
          <label>
            City:
            <input name="city" />
          </label>
          <label>
            Postal code:
            <input name="zipCode" />
          </label>
          <button type="submit">Submit</button>
        </form>
      );
    });
    
    export default ShippingForm;
    label {
      display: block; margin-top: 10px;
    }
    
    input {
      margin-left: 5px;
    }
    
    button[type="button"] {
      margin: 5px;
    }
    
    .dark {
      background-color: black;
      color: white;
    }
    
    .light {
      background-color: white;
      color: black;
    }

    Доволі часто код без запам'ятовування працює добре. Якщо ваші взаємодії є достатньо швидкими, вам може не знадобитися запам'ятовування.

    Майте на увазі, що вам потрібно запустити React у виробничому режимі, вимкнути React Developer Tools та використовувати пристрої, подібні до тих, що мають користувачі вашого застосунку, щоб отримати реалістичне уявлення про те, що насправді сповільнює ваш застосунок.


    Оновлення стану із запам'ятованого зворотного виклику

    Іноді може знадобитися оновити стан на основі попереднього стану із запам'ятованого зворотного виклику.

    Ця функція handleAddTodo визначає todos як залежну, оскільки обчислює наступний todos з неї:

    function TodoList() {
      const [todos, setTodos] = useState([]);
    
      const handleAddTodo = useCallback((text) => {
        const newTodo = { id: nextId++, text };
        setTodos([...todos, newTodo]);
      }, [todos]);
      // ...

    Зазвичай ви хочете, щоб запам'ятовані функції мали якомога менше залежностей. Якщо ви читаєте деякий стан лише для обчислення наступного стану, ви можете вилучити цю залежність, передавши функцію updater замість:

    function TodoList() {
      const [todos, setTodos] = useState([]);
    
      const handleAddTodo = useCallback((text) => {
        const newTodo = { id: nextId++, text };
        setTodos(todos => [...todos, newTodo]);
      }, []); // ✅ No need for the todos dependency
      // ...

    Тут замість того, щоб зробити todos залежністю і прочитати її всередині, ви передаєте інструкцію про те, як як оновити стан (todos => [...todos, newTodo]) до React. Детальніше про функції оновлювача.


    Запобігання ефекту від надто частого застосування

    Іноді вам може знадобитися викликати функцію зсередини Ефекту:

    function ChatRoom({ roomId }) {
      const [message, setMessage] = useState('');
    
      function createOptions() {
        return {
          serverUrl: 'https://localhost:1234',
          roomId: roomId
        };
      }
    
      useEffect(() => {
        const options = createOptions();
        const connection = createConnection();
        connection.connect();
        // ...

    Це створює проблему. Кожне реактивне значення має бути оголошено як залежність вашого ефекту. Однак, якщо ви оголосите createOptions як залежність, це призведе до того, що ваш ефект буде постійно перепідключатися до кімнати чату:

    useEffect(() => {
        const options = createOptions();
        const connection = createConnection();
        connection.connect();
        return () => connection.disconnect();
      }, [createOptions]); // 🔴 Problem: This dependency changes on every render
      // ...

    Щоб вирішити цю проблему, ви можете обгорнути функцію, яку потрібно викликати з ефекту, в useCallback:

    function ChatRoom({ roomId }) {
      const [message, setMessage] = useState('');
    
      const createOptions = useCallback(() => {
        return {
          serverUrl: 'https://localhost:1234',
          roomId: roomId
        };
      }, [roomId]); // ✅ Only changes when roomId changes
    
      useEffect(() => {
        const options = createOptions();
        const connection = createConnection();
        connection.connect();
        return () => connection.disconnect();
      }, [createOptions]); // ✅ Only changes when createOptions changes
      // ...

    Це гарантує, що функція createOptions буде однаковою у різних рендерингах, якщо roomId однакова. Втім, ще краще усунути потребу у функціональній залежності. Перемістіть вашу функцію всередину ефекту:

    function ChatRoom({ roomId }) {
      const [message, setMessage] = useState('');
    
      useEffect(() => {
        function createOptions() { // ✅ No need for useCallback or function dependencies!
          return {
            serverUrl: 'https://localhost:1234',
            roomId: roomId
          };
        }
    
        const options = createOptions();
        const connection = createConnection();
        connection.connect();
        return () => connection.disconnect();
      }, [roomId]); // ✅ Only changes when roomId changes
      // ...

    Тепер ваш код простіший і не потребує useCallback. Дізнайтеся більше про вилучення залежностей ефектів.


    Оптимізація користувацького хука

    Якщо ви пишете користувацький хук, рекомендується обгорнути всі функції, які він повертає, у useCallback:

    function useRouter() {
      const { dispatch } = useContext(RouterStateContext);
    
      const navigate = useCallback((url) => {
        dispatch({ type: 'navigate', url });
      }, [dispatch]);
    
      const goBack = useCallback(() => {
        dispatch({ type: 'back' });
      }, [dispatch]);
    
      return {
        navigate,
        goBack,
      };
    }

    Це гарантує, що споживачі вашого хука зможуть оптимізувати власний код за потреби.


    Налагодження

    Щоразу, коли мій компонент рендерить, useCallback повертає іншу функцію

    Переконайтеся, що ви вказали масив залежностей як другий аргумент!

    Якщо ви забудете масив залежностей, useCallback щоразу повертатиме нову функцію:

    function ProductPage({ productId, referrer }) {
      const handleSubmit = useCallback((orderDetails) => {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }); // 🔴 Returns a new function every time: no dependency array
      // ...

    Виправлена версія, що передає масив залежностей як другий аргумент:

    function ProductPage({ productId, referrer }) {
      const handleSubmit = useCallback((orderDetails) => {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }, [productId, referrer]); // ✅ Does not return a new function unnecessarily
      // ...

    Якщо це не допомогло, то проблема у тому, що принаймні одна з ваших залежностей відрізняється від попереднього рендерингу. Ви можете налагодити цю проблему за допомогою ручного виведення залежностей у консоль:

    const handleSubmit = useCallback((orderDetails) => {
        // ..
      }, [productId, referrer]);
    
      console.log([productId, referrer]);

    Після цього ви можете клацнути правою кнопкою миші на масивах з різних рендерингів у консолі і вибрати "Зберегти як глобальну змінну" для обох. Якщо припустити, що перший масив було збережено як temp1, а другий - як temp2, ви можете скористатися консоллю браузера, щоб перевірити, чи кожна залежність в обох масивах однакова:

    Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
    Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
    Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

    Коли ви знайдете, яка саме залежність порушує запам'ятовування, або знайдіть спосіб її видалити, або запам'ятайте її також.


    Мені потрібно викликати useCallback для кожного елемента списку у циклі, але це не дозволено

    Припустимо, що компонент Chart обгорнуто у memo. Ви хочете пропустити повторний рендеринг кожного Chart у списку при повторному рендерингу компонента ReportList. Однак, ви не можете викликати useCallback у циклі:

    function ReportList({ items }) {
      return (
        <article>
          {items.map(item => {
            // 🔴 You can't call useCallback in a loop like this:
            const handleClick = useCallback(() => {
              sendReport(item)
            }, [item]);
    
            return (
              <figure key={item.id}>
                <Chart onClick={handleClick} />
              </figure>
            );
          })}
        </article>
      );
    }

    Натомість витягніть компонент для окремого елемента і помістіть useCallback туди:

    function ReportList({ items }) {
      return (
        <article>
          {items.map(item =>
            <Report key={item.id} item={item} />
          )}
        </article>
      );
    }
    
    function Report({ item }) {
      // ✅ Call useCallback at the top level:
      const handleClick = useCallback(() => {
        sendReport(item)
      }, [item]);
    
      return (
        <figure>
          <Chart onClick={handleClick} />
        </figure>
      );
    }

    Як варіант, ви можете видалити useCallback в останньому фрагменті і натомість обгорнути сам Report у memo. Якщо проп item не буде змінено, Report пропустить повторний рендеринг, тому Chart також пропустить повторний рендеринг:

    function ReportList({ items }) {
      // ...
    }
    
    const Report = memo(function Report({ item }) {
      function handleClick() {
        sendReport(item);
      }
    
      return (
        <figure>
          <Chart onClick={handleClick} />
        </figure>
      );
    });