Перейти к содержанию

useCallback

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

1
const cachedFn = useCallback(fn, dependencies);

Описание

useCallback(fn, dependencies)

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 вернет вам ту же функцию, если dependencies не изменились с момента последнего рендера. В противном случае, он отдаст вам функцию, которую вы передали во время текущего рендеринга, и сохранит ее на случай, если она может быть использована позже. React не будет вызывать вашу функцию. Функция возвращается вам, чтобы вы могли решить, когда и стоит ли ее вызывать.

dependencies

Список всех реактивных значений, на которые ссылается код fn. Реактивные значения включают пропсы, состояние, а также все переменные и функции, объявленные непосредственно в теле вашего компонента. Если ваш линтер настроен на React, он проверит, что каждое реактивное значение правильно указано в качестве зависимости. Список зависимостей должен иметь постоянное количество элементов и быть написан inline по типу [dep1, dep2, dep3]. React будет сравнивать каждую зависимость с предыдущим значением, используя алгоритм сравнения Object.is.

Возвращаемое значение

При первоначальном рендере useCallback возвращает переданную вами функцию fn.

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

Ограничения

  • useCallback - это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или ваших собственных хуков. Вы не можете вызывать его внутри циклов или условий. Если вам это нужно, создайте новый компонент и перенесите состояние в него.
  • React не будет выбрасывать кэшированную функцию, если для этого нет особой причины. Например, в разработке React выбрасывает кэш, когда вы редактируете файл вашего компонента. Как в разработке, так и в продакшене, React отбрасывает кэш, если ваш компонент приостанавливается во время начального монтирования. В будущем React может добавить больше функций, которые будут использовать преимущества отбрасывания кэша - например, если React в будущем добавит встроенную поддержку виртуализированных списков, то будет иметь смысл отбрасывать кэш для элементов, которые прокручиваются из области просмотра виртуализированной таблицы. Это должно соответствовать вашим ожиданиям, если вы полагаетесь на useCallback в качестве оптимизации производительности. В противном случае, более подходящими могут быть переменные состояния или ref.

Использование

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

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

Чтобы кэшировать функцию между повторными рендерингами компонента, оберните ее определение в хук useCallback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { useCallback } from 'react';

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

Вам нужно передать две вещи в useCallback:

  1. Определение функции, которую вы хотите кэшировать между повторными рендерами.
  2. Список зависимостей, включающий каждое значение в вашем компоненте, которое используется в вашей функции.

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

При последующих рендерах React будет сравнивать зависимости с зависимостями, которые вы передали во время предыдущего рендера. Если ни одна из зависимостей не изменилась (по сравнению с Object.is), useCallback вернет ту же функцию, что и раньше. В противном случае, useCallback вернет функцию, которую вы передали на этом рендере.

Другими словами, useCallback кэширует функцию между повторными рендерами, пока ее зависимости не изменятся.

Давайте рассмотрим пример, чтобы увидеть, когда это полезно.

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

1
2
3
4
5
6
7
8
9
function ProductPage({ productId, referrer, theme }) {
    // ...
    return (
        <div className={theme}>
            <ShippingForm onSubmit={handleSubmit} />
        </div>
    );
    // ...
}

Вы заметили, что при переключении параметра theme приложение на мгновение замирает, но если убрать <ShippingForm /> из JSX, то все работает быстро. Это говорит о том, что стоит попробовать оптимизировать компонент ShippingForm.

По умолчанию, когда компонент рендерится, React рекурсивно рендерит все его дочерние компоненты. Вот почему, когда ProductPage рендерится с другой theme, компонент ShippingForm также рендерится. Это хорошо для компонентов, которые не требуют больших вычислений для повторного рендеринга. Но если вы убедились, что повторный рендеринг медленный, вы можете сказать ShippingForm пропустить повторный рендеринг, когда его пропсы такие же, как и при последнем рендере, обернув его в memo:

1
2
3
4
5
6
7
import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({
    onSubmit,
}) {
    // ...
});

После этого изменения ShippingForm будет пропускать повторный рендеринг, если все его пропсы те же, что и при последнем рендеринге. Вот когда кэширование функции становится важным! Допустим, вы определили handleSubmit без useCallback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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, вы гарантируете, что это будет одна и та же функция между повторными рендерами (пока не изменятся зависимости). Вы не обязаны обертывать функцию в useCallback, если только вы не делаете это по какой-то конкретной причине. В данном примере причина в том, что вы передаете ее компоненту, обернутому в memo, и это позволяет ему пропустить повторный рендеринг. Есть и другие причины, по которым вам может понадобиться useCallback, которые описаны далее на этой странице.

useCallback только для оптимизации

Вы должны полагаться на useCallback только в качестве оптимизации производительности. Если ваш код не работает без него, найдите основную проблему и сначала устраните ее. Затем вы можете добавить useCallback обратно.

Как useCallback связан с useMemo?

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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), чтобы он не изменился, если product не изменился. Это позволяет передавать объект requirements вниз без ненужного повторного рендеринга ShippingForm. При необходимости React будет вызывать переданную вами функцию во время рендеринга для вычисления результата.
  • useCallback кэширует саму функцию. В отличие от useMemo, он не вызывает предоставленную вами функцию. Вместо этого она кэширует предоставленную вами функцию, так что handleSubmit сама по себе не изменяется, если только productId или referrer не изменились. Это позволяет вам передавать функцию handleSubmit вниз без ненужного повторного рендеринга ShippingForm. Ваш код не будет выполняться до тех пор, пока пользователь не отправит форму.

Если вы уже знакомы с useMemo, вам будет полезно представить useCallback следующим образом:

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

Подробнее о разнице между useMemo и useCallback .

Должны ли вы везде добавлять useCallback?

Если ваше приложение похоже на этот сайт, и большинство взаимодействий являются грубыми (например, замена страницы или целого раздела), мемоизация обычно не нужна. С другой стороны, если ваше приложение больше похоже на редактор рисунков, и большинство взаимодействий являются гранулированными (например, перемещение фигур), то мемоизация может оказаться очень полезной.

Кэширование функции с useCallback полезно только в нескольких случаях:

  • Вы передаете ее в качестве параметра компоненту, обернутому в memo. Вы хотите пропустить повторный рендеринг, если значение не изменилось. Мемоизация позволяет вашему компоненту повторно отображаться только в том случае, если изменились зависимости.
  • Функция, которую вы передаете, позже будет использоваться как зависимость какого-то Hook. Например, другая функция, обернутая в useCallback, зависит от нее, или вы зависите от этой функции из useEffect.

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

Обратите внимание, что useCallback не предотвращает создание функции. Вы всегда создаете функцию (и это хорошо!), но React игнорирует это и возвращает вам кэшированную функцию, если ничего не изменилось.

На практике вы можете сделать ненужной мемоизацию, следуя нескольким принципам:.

  1. Когда компонент визуально оборачивает другие компоненты, пусть он принимает JSX в качестве дочерних компонентов Тогда, если компонент-обертка обновляет свое состояние, React знает, что его дочерние компоненты не нужно перерисовывать.
  2. Предпочитайте локальное состояние и не поднимайте состояние вверх дальше, чем это необходимо. Не храните переходные состояния, такие как формы и то, наведен ли элемент на вершину вашего дерева или в глобальной библиотеке состояний.
  3. Сохраняйте чистоту логики рендеринга Если повторный рендеринг компонента вызывает проблему или приводит к заметным визуальным артефактам, это ошибка в вашем компоненте! Исправьте ошибку вместо того, чтобы добавлять мемоизацию.
  4. Избегайте ненужных Эффектов, обновляющих состояние Большинство проблем с производительностью в приложениях React вызваны цепочками обновлений, исходящих от Эффектов, которые заставляют ваши компоненты рендериться снова и снова.
  5. Попробуйте удалить ненужные зависимости из ваших Эффектов. Например, вместо мемоизации часто проще переместить какой-то объект или функцию внутрь Эффекта или за пределы компонента.

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

Разница между useCallback и объявлением функции напрямую

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

В этом примере компонент ShippingForm искусственно замедлен, чтобы вы могли увидеть, что происходит, когда React-компонент, который вы рендерите, действительно медленный. Попробуйте увеличить счетчик и переключить тему.

Увеличение счетчика кажется медленным, потому что это заставляет замедленный ShippingForm перерисовываться. Это ожидаемо, потому что счетчик изменился, и вам нужно отразить новый выбор пользователя на экране.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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'}
            />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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</code> is
                    artificially slowed down!
                </b>
            </p>
            <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;

2. Всегда перерендеринг компонента

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

В отличие от предыдущего примера, переключение темы теперь также происходит медленно! Это происходит потому, что в этой версии нет вызова useCallback, поэтому handleSubmit всегда является новой функцией, и замедленный компонент ShippingForm не может пропустить повторный рендеринг.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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'}
            />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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</code> is
                    artificially slowed down!
                </b>
            </p>
            <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;

Однако, вот тот же код с искусственным замедлением. Отсутствие useCallback ощущается заметно или нет?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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'}
            />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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;

Довольно часто код без мемоизации работает нормально. Если ваши взаимодействия достаточно быстрые, мемоизация не нужна.

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

Обновление состояния из мемоизированного обратного вызова

Иногда вам может потребоваться обновить состояние на основе предыдущего состояния из мемоизированного обратного вызова.

Эта функция handleAddTodo указывает todos как зависимость, потому что она вычисляет следующий todos из него:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function TodoList() {
    const [todos, setTodos] = useState([]);

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

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

1
2
3
4
5
6
7
8
9
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 зависимостью и читать ее внутри, вы передаете в React инструкцию о том, как обновить состояние (todos => [...todos, newTodo]). Подробнее о функциях обновления .

Предотвращение слишком частого срабатывания эффекта

Иногда вы можете захотеть вызвать функцию внутри Effect:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 как зависимость, это заставит ваш Эффект постоянно переподключаться к чату:

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

Чтобы решить эту проблему, вы можете обернуть функцию, которую нужно вызвать из Effect, в useCallback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 одинаков. Однако, еще лучше устранить необходимость в зависимости от функции. Переместите вашу функцию внутрь Effect:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 будет возвращать каждый раз новую функцию:

1
2
3
4
5
6
7
8
9
function ProductPage({ productId, referrer }) {
    const handleSubmit = useCallback((orderDetails) => {
        post('/product/' + productId + '/buy', {
            referrer,
            orderDetails,
        });
    }); // 🔴 Каждый раз возвращает новую функцию: нет массива зависимостей
    // ...
}

Это исправленная версия, передающая массив зависимостей в качестве второго аргумента:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function ProductPage({ productId, referrer }) {
    const handleSubmit = useCallback(
        (orderDetails) => {
            post('/product/' + productId + '/buy', {
                referrer,
                orderDetails,
            });
        },
        [productId, referrer]
    ); // ✅ Не возвращает новую функцию без необходимости
    // ...
}

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

1
2
3
4
5
6
7
8
const handleSubmit = useCallback(
    (orderDetails) => {
        // ..
    },
    [productId, referrer]
);

console.log([productId, referrer]);

Затем вы можете щелкнуть правой кнопкой мыши на массивах из разных рендеров в консоли и выбрать "Store as a global variable" для обоих. Предположив, что первый массив был сохранен как temp1, а второй - как temp2, вы можете использовать консоль браузера, чтобы проверить, является ли каждая зависимость в обоих массивах одинаковой:

1
2
3
4
5
6
7
8
// Является ли первая зависимость одинаковой для всех массивов?
Object.is(temp1[0], temp2[0]);

// Является ли вторая зависимость одинаковой для всех массивов?
Object.is(temp1[1], temp2[1]);

// ... и так далее для каждой зависимости...
Object.is(temp1[2], temp2[2]);

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

Мне нужно вызвать useCallback для каждого элемента списка в цикле, но это не разрешено

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 тоже пропускает повторный рендеринг:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function ReportList({ items }) {
    // ...
}

const Report = memo(function Report({ item }) {
    function handleClick() {
        sendReport(item);
    }

    return (
        <figure>
            <Chart onClick={handleClick} />
        </figure>
    );
});

Источник — https://react.dev/reference/react/useCallback

Комментарии