Всем привет! На связи Spectr и рубрика «Что читают наши разработчики». Сегодня разберем оптимизацию функциональных компонентов за счет мемоизации функций обратного вызова с помощью useCallback().
Этот материал — перевод статьи. Мы не меняем содержание и приводим слова автора так, как это было написано в оригинале. Если у вас есть предложения или свой опыт в подобных методиках, опишите их в комментариях — мы с радостью ответим и расскажем о своих практиках. Давайте обмениваться опытом!
Введение
Оптимизация производительности в современной веб-разработке имеет ключевое значение для создания более плавного пользовательского опыта. React, популярная библиотека JavaScript для создания пользовательских интерфейсов, предоставляет несколько хуков, которые помогают разработчикам эффективно управлять состоянием и побочными эффектами. Один из таких хуков — useCallback(), который играет важную роль в оптимизации функциональных компонентов. Это происходит за счет мемоизации функций обратного вызова.
Независимо от того, начинающий вы разработчик React или хотите углубить свои знания, это руководство предоставит вам полезную информацию о useCallback(), включая его синтаксис, применение, распространенные случаи использования и лучшие практики. Мы также сравним его с другим хуком — useMemo().
Понимание функций обратного вызова в React
Прежде чем объяснять useCallback(), начнем с понимания функций обратного вызова. В JavaScript функция обратного вызова — это функция, которая передается в другую функцию в качестве аргумента и выполняется после наступления определенного события или действия.
Функции обратного вызова работают в React так же, как и в JavaScript. Также их можно передавать в дочерние компоненты через props. Это позволяет дочерним компонентам взаимодействовать с родительским компонентом, обеспечивая передачу данных и выполнение действий вверх по дереву компонентов.
Функции обратного вызова — неотъемлемая часть React-приложений, поскольку они выполняют такие задачи, как асинхронные операции (например, сетевые запросы) или обработка событий (например, кликов, отправки форм или других взаимодействий с пользователем). Например, обработчик события onClick кнопки может быть определен как функция обратного вызова, которая обновляет состояние React-компонента или выполняет какое-либо действие при нажатии на кнопку.
Пример:
const MyComponent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<button onClick={handleClick}>
Click me: {count}
</button>
);
};
В этом примере handleClick — это функция обратного вызова, которая увеличивает состояние count при каждом нажатии на кнопку.
На первый взгляд, функции обратного вызова кажутся очень удобными и выполняют свою задачу. Так зачем же нам нужен useCallback()? Дело в том, что использование функции обратного вызова не всегда оптимально, как мы увидим в следующем разделе.
Проблема с функциями обратного вызова
Несмотря на их полезность в разработке React, особенно при обработке событий и взаимодействий, функции обратного вызова могут приводить к проблемам с производительностью, если их использовать неправильно. Основная проблема связана с тем, как React обрабатывает повторные рендеры компонентов и как JavaScript рассматривает функции как объекты.
Рассмотрим некоторые распространенные проблемы, связанные с функциями обратного вызова в React:
Необходимые повторные рендеры
В JavaScript функции считаются объектами. Это приводит к тому, что при каждом рендере или повторном рендере компонента любая функция внутри него пересоздается как новый объект. Таким образом, даже если логика функции остается абсолютно неизменной, React будет воспринимать ее как новую сущность каждый раз.
Рассмотрим следующий пример:
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('Button clicked');
};
return (
<div>
<p>Count: {count}</p>
<ChildComponent onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
В этом примере каждый раз, когда значение count изменяется и ParentComponent перерендеривается, создается новая функция handleClick. С точки зрения React, эта новая функция отличается от той, которая была создана при предыдущем рендере, даже если ее функциональность осталась неизменной.
Сравнение prop и повторных рендеров дочерних компонентов
Когда новая функция создается и передается в качестве props, React воспринимает это как изменение props. Это может привести к повторному рендеру дочернего компонента, если он использует оптимизационные техники, такие как React.memo() или PureComponent.
Рассмотрим пример:
const ChildComponent = React.memo(({ onClick }) => {
console.log('Child component rendered');
return <button onClick={onClick}>Click me</button>;
});
Даже при использовании React.memo, который предназначен для предотвращения ненужных рендеров, ChildComponent будет перерендериваться каждый раз, когда ParentComponent рендерится, потому что он получает «новый» prop onClick при каждом рендере родительского компонента.
Устаревшие замыкания (Stale Closures)
Функции обратного вызова иногда могут «захватывать» устаревшие значения из своего окружения, что приводит к появлению трудноуловимых и сложных для исправления ошибок.
Рассмотрим пример:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(`Count is: ${count}`);
}, 1000);
return () => clearInterval(timer);
}, []);
return <button onClick={() => setCount(count + 1)}>Increment</button>;
}
В этом компоненте счетчика функция обратного вызова, используемая в setInterval, всегда будет выводить 0 в консоль. Это происходит потому, что замыкание захватывает начальное значение count на момент первого рендера и не обновляется при изменении состояния.
Эти проблемы приводят к следующим последствиям для производительности:
Каскадные повторные рендеры. В глубоко вложенных деревьях компонентов ненужные повторные рендеры могут каскадно распространяться вниз, вызывая значительную нагрузку на производительность.
Ненужные вычисления. Каждый повторный рендер включает процесс согласования (reconciliation) в React, который может быть вычислительно затратным, особенно для сложных компонентов React.
Увеличение потребления памяти. Постоянное создание новых экземпляров функций может привести к увеличению потребления памяти с течением времени.
Эти проблемы подчеркивают необходимость управления функциями обратного вызова в React-приложениях. useCallback() был разработан для решения этих проблем, и мы подробно рассмотрим его в следующих разделах.
Что такое useCallback()
useCallback() — это один из встроенных хуков React, который оптимизирует производительность React-приложений, предотвращая ненужные повторные рендеры компонентов. Он достигает этого с помощью мемоизации функций обратного вызова, гарантируя, что функции будут пересоздаваться только тогда, когда изменяется массив зависимостей.
Синтаксис и параметры
Синтаксис useCallback() выглядит следующим образом:
const memoizedCallback = useCallback(() => {
// Your callback logic here
}, [dependency1, dependency2, ...]);
Хук useCallback() принимает два аргумента:
1. Функция обратного вызова — это первый аргумент, который представляет функцию, которую нужно мемоизировать (запомнить).
2. Массив зависимостей — это второй аргумент, представляющий массив зависимостей, который определяет, когда функция обратного вызова должна быть пересоздана. Если какое-либо значение в этом массиве изменяется между рендерами, функция обратного вызова будет пересоздана.
Как работает useCallback()
После добавления useCallback() React будет мемоизировать экземпляр переданной функции между повторными рендерами. Вот что происходит:
1. При начальном рендере React создает функцию и возвращает ее.
2. При последующих рендерах React будет:
проверять, изменились ли значения в массиве зависимостей;
если ничего не изменилось, он возвращает тот же экземпляр функции, что и при предыдущем рендере;
если что-то изменилось, React создает новый экземпляр функции и возвращает его.
Это означает, что пока зависимости не изменяются, на каждом рендере будет возвращаться один и тот же экземпляр функции.
Рассмотрим пример, который демонстрирует, как работает useCallback():
function ParentComponent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []); // Empty dependency array means this function never changes
return (
<div>
<p>Count: {count}</p>
<ChildComponent onIncrement={increment} />
</div>
);
}
const ChildComponent = React.memo(({ onIncrement }) => {
console.log('ChildComponent rendered');
return <button onClick={onIncrement}>Increment</button>;
});
В этом примере:
— increment — это мемоизированная функция, использующая useCallback(). Она будет пересоздана только в том случае, если массив зависимостей, который в данном случае пуст, изменится.
— ChildComponent обернут в React.memo(), чтобы предотвратить ненужные повторные рендеры компонента. Он будет перерендериваться только в случае изменения props.
— Поскольку increment мемоизирована, ChildComponent не будет перерендериваться при изменении состояния count в ParentComponent.
Теперь, когда мы понимаем, как работает useCallback() и как правильно его использовать, давайте рассмотрим, почему знание и использование этого хука может быть полезным при разработке React-приложений.
Когда использовать useCallback()
Вот несколько сценариев, когда использование useCallback() особенно полезно:
1. Оптимизация производительности в списках. Когда рендерится список элементов, возможно, вам нужно передать функцию обратного вызова каждому элементу. Использование useCallback() гарантирует, что каждый элемент получит одну и ту же функцию, предотвращая ненужные повторные рендеры.
2. Когда функция обратного вызова является зависимостью в useEffect(). Используйте useCallback(), когда функция обратного вызова является зависимостью в хуке useEffect(), чтобы избежать ненужного выполнения эффекта.
3. При работе с кастомными хуками. Используйте useCallback(), чтобы гарантировать консистентность ссылок на функции при создании кастомных хуков, которые возвращают функции обратного вызова, особенно если эти функции будут использоваться в качестве зависимостей в эффектах.
4. Предотвращение устаревших замыканий. Когда функция обратного вызова зависит от состояния или props, использование useCallback() помогает гарантировать, что функция всегда будет использовать актуальные значения.
5. Передача функций обратного вызова в дочерние компоненты. Если ссылки на функцию изменяются, дочерние компоненты могут перерендериваться без необходимости. useCallback() помогает предотвратить это, мемоизируя функцию и гарантируя, что она изменится только тогда, когда изменятся ее зависимости.
Когда не использовать useCallback()
Хотя useCallback() — мощный инструмент для оптимизации, его использование может быть излишним в следующих случаях:
1. Для простых компонентов React, которые не перерендериваются часто. Если компонент не рендерится часто, использование useCallback() не принесет значительной пользы и только добавит лишнюю сложность.
2. Когда функция обратного вызова используется только внутри компонента и не передается как props или не используется в массиве зависимостей. Если функция не передается в дочерние компоненты или не используется в useEffect(), мемоизация функции не имеет смысла.
3. Если выигрыш в производительности незначителен по сравнению с добавленной сложностью. Если добавление useCallback() не приводит к заметному улучшению производительности, лучше избежать его использования, чтобы не усложнять код.
Важно всегда использовать хуки правильно, чтобы избежать ненужной сложности или проблем с производительностью в кодовой базе.
Как использовать useCallback(). Практические примеры
Все, что было обсуждено, не имеет большого смысла, если мы не можем применить эти знания в реальных приложениях. Поэтому теперь мы рассмотрим использование useCallback() в приложении электронной коммерции. Для этого примера мы сосредоточимся на двух функциях:
Добавление товаров в «Избранное».
Добавление товаров в «Корзину».
Для реализации этих функций мы будем использовать Hygraph, GraphQL-ориентированную CMS (систему управления контентом). Hygraph упрощает процесс разработки, предоставляя возможности для управления контентом, позволяя разработчикам сосредоточиться на создании функционала.
Создание React приложения
Настройте новое React приложение с помощью Vite, выполнив следующую команду:
npm create vite@latest
Выберите React в качестве фреймворка и завершите процесс установки. Также установите TailwindCSS для стилизации.
Запустите сервер разработки:
npm run dev
Теперь проверьте терминал, чтобы узнать порт, на котором работает приложение.
Настройка проекта Hygraph
Сначала зарегистрируйтесь на бесплатный аккаунт для разработчиков, если вы еще этого не сделали.
Чтобы использовать Hygraph в React приложении, выполните следующие шаги:
1. Клонируйте проект. Клонируйте этот стартовый проект Hygraph для электронной коммерции, чтобы быстро настроить витрину. После клонирования откройте проект.
2. Настройте доступ к API. В проекте Hygraph перейдите в Project Settings > API Access > Public Content API. Настройте разрешения для Public Content API, чтобы разрешить чтение данных без аутентификации. Нажмите «Yes, initialize defaults», чтобы добавить необходимые разрешения для High Performance Content API.
3. Установите переменную окружения. Найдите High Performance Content API и скопируйте URL. В корневой папке React приложения создайте файл `.env` и добавьте URL, как показано ниже:
VITE_HYGRAPH_HIGH_PERFORMANCE_ENDPOINT=YOUR_URL
Замените YOUR_URL на соответствующий URL.
Перед тем как начать работать с React-приложением, исследуйте Hygraph, чтобы понять доступный контент. Перейдите в API playground Hygraph и выполните следующий запрос для получения данных о товарах:
query GetProducts {
products {
images {
id
url
}
name
price
id
}
}
Этот запрос извлекает информацию о товарах, доступную через раздел Content. Теперь давайте получим и отобразим данные о товарах в нашем React-приложении.
Получение данных из Hygraph
Перейдите в терминал проекта и установите клиент GraphQL для получения данных:
npm add graphql-request
Откройте `App.js` и добавьте следующий код для получения и отображения продуктов:
import { useState, useEffect } from "react";
import { GraphQLClient, gql } from "graphql-request";
import "./App.css";
const endpoint = new GraphQLClient(
import.meta.env.VITE_HYGRAPH_HIGH_PERFORMANCE_ENDPOINT
);
const GET_PRODUCTS_QUERY = gql`
query GetProducts {
products {
images {
id
url
}
name
price
id
}
}
`;
function App() {
const [products, setProducts] = useState([]);
useEffect(() => {
const fetchProducts = async () => {
try {
const data = await endpoint.request(GET_PRODUCTS_QUERY);
setProducts(data.products);
} catch (error) {
console.error("Error fetching products:", error);
}
};
fetchProducts();
}, []);
return (
<div className="App container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Список продуктов</h1>
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{products.map((product) => (
<li
key={product.id}
className="border rounded-lg p-4 shadow-md bg-gray-50">
<img
src={product.images[0].url}
alt={product.name}
className="w-full object-cover mb-4 rounded bg-white"
/>
<h2 className="text-lg font-semibold mb-2">{product.name}</h2>
<p className="text-gray-600 mb-2">
Цена: ${(product.price / 100).toFixed(2)}
</p>
</li>
))}
</ul>
</div>
);
}
export default App;
Код выше получает данные из Hygraph с использованием High Performance Content API. Мы также использовали хук [useState] (https://hygraph.com/blog/usestate-react), который управляет состоянием данных продуктов, а хук `useEffect` обрабатывает получение данных.
На этом этапе у вас должен быть экран, который выглядит как показано здесь:
Теперь давайте добавим функцию для пометки продуктов как «избранных», чтобы продемонстрировать полезность useCallback().
Добавление функционала «Избранное»
Сначала установите FontAwesome для иконок:
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome
Обновите `App.js`, чтобы включить хук useCallback() и иконки FontAwesome:
import { useState, useEffect, useCallback } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHeart } from "@fortawesome/free-solid-svg-icons";
В этом случае мы используем иконку сердца, чтобы продемонстрировать функционал избранного в интерфейсе.
Далее добавим состояние:
const [favorites, setFavorites] = useState({});
Это инициализирует переменную состояния favorites как пустой объект и предоставляет функцию setFavorites для обновления состояния. Этот объект функции позволяет переключаться между двумя состояниями, такими как «в избранном» или «не в избранном».
Далее, мы объявим useCallback():
const handleFavorite = useCallback((id) => {
setFavorites((prevFavorites) => ({
...prevFavorites,
[id]: !prevFavorites[id],
}));
}, []);
Здесь мы достигли следующего:
Обернули функцию handleFavorite в useCallback(), чтобы запомнить ее и предотвратить ненужные пересоздания при повторных рендерах. Это особенно полезно для производительности, если продуктовый элемент будет использоваться в больших приложениях с множеством продуктов.
Функция также переключает статус избранного продукта, обновляя состояние favorites.
Теперь мы завершим это, добавив следующий код под раздел «Цена»:
<button
className="text-xl mr-2 heart-button"
onClick={() => handleFavorite(product.id)}>
<FontAwesomeIcon
icon={faHeart}
style={{ color: favorites[product.id] ? "red" : "grey" }}
/>
</button>
Здесь мы убедились, что:
когда пользователь нажимает кнопку, она вызывает функцию handleFavorite, передавая ID продукта;
цвет иконки сердца меняется в зависимости от статуса избранного, который хранится в объекте состояния favorites.
Теперь мы должны увидеть иконку избранного в интерфейсе для каждого продукта.
Использование хука useCallback() для переключения избранных товаров для каждого продукта помогает предотвратить ненужные пересоздания функции handleFavorite при повторных рендерах. Эта оптимизация критична в сценариях с большим количеством элементов. Теперь давайте добавим функционал добавления в корзину.
Реализация функции «Добавить в корзину»
Сначала обновите импорты FontAwesome, чтобы включить иконки корзины покупок:
import {
faHeart,
faCartShopping,
faPlus,
faMinus,
} from "@fortawesome/free-solid-svg-icons";
Добавьте состояние и функции для работы с корзиной:
const [cart, setCart] = useState({});
const handleAddToCart = useCallback((product) => {
setCart((prevCart) => {
const quantity = prevCart[product.id]
? prevCart[product.id].quantity + 1
: 1;
return {
...prevCart,
[product.id]: { ...product, quantity },
};
});
}, []);
const handleRemoveFromCart = useCallback((productId) => {
setCart((prevCart) => {
if (!prevCart[productId]) return prevCart;
const newQuantity = prevCart[productId].quantity - 1;
if (newQuantity > 0) {
return {
...prevCart,
[productId]: { ...prevCart[productId], quantity: newQuantity },
};
} else {
const newCart = { ...prevCart };
delete newCart[productId];
return newCart;
}
});
}, []);
Здесь у нас есть две функции, которые выполняют следующие действия:
handleAddToCart. Функция добавляет продукт в корзину или увеличивает его количество, если он уже есть в корзине.
handleRemoveFromCart. Эта функция уменьшает количество продукта в корзине или удаляет его, если количество достигает 0.
Общие функции используют useCallback() для мемоизации и setCart для обновления состояния корзины.
Обновите файл, чтобы добавить элементы управления корзиной:
<div className="flex justify-center items-center mt-3">
<FontAwesomeIcon icon={faCartShopping} className="mr-2" />
<button
className="bg-gray-300 hover:bg-gray-400 rounded-full px-1"
onClick={() => handleAddToCart(product)}
>
<FontAwesomeIcon icon={faPlus} />
</button>
<span className="mx-2">
{cart[product.id] ? cart[product.id].quantity : 0}
</span>
<button
className="bg-gray-300 hover:bg-gray-400 rounded-full px-1"
onClick={() => handleRemoveFromCart(product.id)}
>
<FontAwesomeIcon icon={faMinus} />
</button>
</div>
Наконец, добавьте раздел для отображения содержимого корзины:
<div className="cart mt-8 p-4 border-t">
<h2 className="text-xl font-bold">
Корзина ({Object.keys(cart).length} товаров)
</h2>
<ul className="mt-2">
{Object.values(cart).map((item, index) => (
<li key={index} className="mb-2 flex justify-between">
<span>
{item.name} - ${(item.price / 100).toFixed(2)}
</span>
<span>Количество: {item.quantity}</span>
</li>
))}
</ul>
</div>
Ваш пользовательский интерфейс теперь должен выглядеть следующим образом:
Использование useCallback() в этом приложении помогает улучшить производительность по мере увеличения количества продуктов и взаимодействий. Оно гарантирует, что функции, обрабатывающие обновления внутреннего состояния, не создаются заново без необходимости.
Вы можете получить доступ к коду на GitHub.
useCallback() и useMemo()
В React хуки useCallback() и useMemo() оба используются для оптимизации производительности путем мемоизации функций и значений, но они предназначены для разных целей и применяются в разных сценариях.
Синтаксис useMemo() выглядит следующим образом и принимает два аргумента (аналогично useCallback()):
Функция для вычисления значения.
Массив зависимостей.
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b], // Массив зависимостей
);
При сравнении useCallback() и useMemo() различия лучше всего рассматривать с точки зрения того, что они мемоизируют, что возвращают и в каких случаях используются, поскольку это помогает избежать путаницы между этими двумя хуками.
Критерии |
useCallback() |
useMemo() |
Что мемоизируют |
Мемоизирует функцию обратного вызова |
Мемоизирует вычисленное значение |
Возвращаемое значение |
Возвращает мемоизированную функцию |
Возвращает мемоизированное значение (может быть любого типа) |
Использование |
Обычно для оптимизации повторных рендеров дочерних компонентов и в массивах зависимостей других хуков |
Для сложных и ресурсоемких вычислений, а также для создания объектов, которые должны оставаться стабильными |
Рассмотрим следующий пример использования useMemo():
import React, { useMemo } from 'react';
const computeExpensiveValue = (a, b) => {
console.log('Computing expensive value...');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += a + b;
}
return result;
};
const App = () => {
const a = 5;
const b = 10;
const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
console.log(`Expensive value for a=${a} and b=${b}: ${expensiveValue}`);
return null;
};
export default App;
В этом примере useMemo() мемоизирует результат выполнения функции computeExpensiveValue(). Вычисление будет выполняться только тогда, когда значения `a` или `b` изменятся. Это позволяет избежать ненужных пересчетов при каждом рендере компонента.
Если бы мы использовали useCallback(), то мемоизировалась бы сама функция computeExpensiveValue(), но дорогостоящие вычисления выполнялись бы каждый раз при вызове этой функции, даже если `a` и `b` остались бы неизменными.
Теперь, когда мы разобрались с разницей между useCallback() и useMemo(), давайте рассмотрим, когда лучше использовать useCallback().
Лучшие практики использования useCallback()
Используйте только на верхнем уровне компонента. Как и любой другой хук, useCallback() должен использоваться только на верхнем уровне компонента и вне любых циклов или условий.
Сочетайте с [React.memo()] (https://hygraph.com/blog/react-memo). Используйте useCallback() вместе с React.memo() для дочерних компонентов, чтобы предотвратить их ненужный повторный рендер, если ссылка на функцию обратного вызова не изменилась.
Уделяйте внимание массиву зависимостей. Массив зависимостей играет ключевую роль. Пропуск необходимых зависимостей может привести к ошибкам. Включайте в массив все значения из области видимости компонента, от которых зависит функция обратного вызова.
Используйте только при необходимости. Применяйте useCallback() только тогда, когда это действительно необходимо. Используйте его только в тех случаях, когда выгода для производительности очевидна.
Заключение
Это руководство предоставляет обзор React-хука useCallback(), начиная с описания решаемой им проблемы и заканчивая примерами использования и лучшими практиками его реализации.
Присоединяйтесь к сообществу разработчиков Hygraph, чтобы узнать о захватывающих инновациях и установить связь с другими разработчиками. Это также отличное место для взаимодействия с командой Hygraph, обмена опытом и расширения своих навыков.
Комментарии (11)
winkyBrain
26.12.2024 19:10При последующих рендерах React будет:
проверять, изменились ли значения в массиве зависимостей;
если ничего не изменилось, он возвращает тот же экземпляр функции, что и при предыдущем рендере;
если что-то изменилось, React создает новый экземпляр функции и возвращает его.
У вас явно пробелы в понимании работы useCallback. Всегда будет создаваться новая анонимная функция, после чего сравнятся зависимости в массиве, и уже потом вам вернётся либо ранее сохранённая функция с прежней ссылкой, либо новая, и она же сохранится вместо старой, для последующих рендеров. Это даже не реакт, а банальное понимание того, как функции и замыкания работают в принципе. useCallback это та же функция, каким термином её ни назови
Как уже написали выше, всё "руководство" по использованию данного хука сводится к одному короткому абзацу. Всё остальное вода, которую лили на хабре до вас, и будут лить после
donatello2005
26.12.2024 19:10Всегда будет создаваться новая анонимная функция
...
Это даже не реакт, а банальное понимание того, как функции и замыкания работают в принципе
Простите, но вы не правы. Ваши доводы логичны, но вы упускаете одну важную деталь, из-за чего все ваши выводы неверны.
useCallback
запускается не в чистом JS, а в JSX. А если быть точнее, проходит через транспиляцию в AST (абстрактное синтаксическое дерево), и далее React работает уже с ним. И это касается не только дерева элементов вreturn
, но и всего кода внутри функции компонента (включая и все хуки).Достаточно простое доказательство этого - это новая версия React, в которой, как пообещали разработчики,
useCallback
писать не потребуется, ко функциям внутри компонента будет автоматически применяться мемоизация на основании анализа кода компилятором.Также вы можете посмотреть скомпилированный в JS код, и вы там увидите, что функция из
useCallback
на самом деле находится за пределами функции компонента и перезаписывается там же (советую включить в React компиляцию компонентов по отдельным бандлам, чтобы проще было читать код, не было лишней логики).Я рекомендовал бы вам прочитать статью про работу AST (на ней работает большинство современных js-фреймворков, типа React, Vue, Svelte и другие): https://habr.com/ru/companies/ruvds/articles/415269/
winkyBrain
26.12.2024 19:10Простите, но вы не правы. Ваши доводы логичны, но вы упускаете одну важную деталь, из-за чего все ваши выводы неверны
Да нет, скорее вы упорно не хотите понимать, как работает javascript) jsx или нет - это всё ещё JS. и в моём мире функция useCallback с входными аргументами X и Y(где X анонимная функция и Y массив зависимостей) в момент вызова уже получает на вход два этих аргумента. То есть она не может вызваться только со вторым аргументом, а когда тот не прошёл проверку сказать "ну лааадно, давайте сюда первый, запишу его вместо старой функции". Понимаете? Как только useCallback был вызван, он уже получил на вход всё необходимое, в том числе новую анонимную функцию, на каждый рендер. Достаточно заглянуть в исходники;)
Достаточно простое доказательство этого - это новая версия React, в которой, как пообещали разработчики,
useCallback
писать не потребуется, ко функциям внутри компонента будет автоматически применяться мемоизация на основании анализа кода компилятором.Доказательство простите чего? Того, что анонимная функция не будет создаваться каждый раз? Не вижу связи абсолютно. Если вы не читали о том, как это будет работать - оно просто само будет решать, что обернуть в useCallback(или в прочие мемоизации), а что нет. Не представляю, как это может что-либо доказывать)
donatello2005
26.12.2024 19:10Простите, вы оказались абсолютно правы. Оказалось, что использование глубокой пересборки кода с помощью AST - это было только в моих мечтах. Насмотревшись на магию SvelteJS и аналогичных решений с построчной обработкой кода, я предполагал, что и в ReactJS эту же магию используют в компиляторе, а-ля:
const cb = useCallback(() => { console.log(`${someFirstDynamicValue}-${someSecondDynamicValue}`); }, [someFirstDynamicValue, someSecondDynamicValue]);
->
callbacksMap[123] = someFirstDynamicValue === callbacksMap[123].dependencies[0] && someSecondDynamicValue === callbacksMap[123].dependencies[1] ? callbacksMap[123] : () => { console.log(`${someFirstDynamicValue}-${someSecondDynamicValue}`); };
Но в финале всё оказалось намного прозаичнее. Действительно, мы при каждом рендере создаём новую функцию внутри
useCallback
. Как описывают это сами разработчики ReactJS:The default behavior of
useCallback
is rather naïve but it is very easy to predict, and you always know that you're gonna "see" fresh values there.Наверное, это правильно с их стороны. Но в этом случае я теперь 100 раз подумаю, нужно ли мне использовать
useCallback
или нет.Вот пример, как проблему с пересозданием функции можно решить (может, кому-то это пригодится):
function useOptimizedCallback<T extends (...args: any[]) => any>(fn: T, deps: readonly unknown[]): T { const ref: any = useRef(fn); // we copy a ref to the callback scoped to the current state/props on each render useLayoutEffect(() => { ref.current = fn; }, deps); return useCallback( (...args: any[]) => ref.current.apply(void 0, args), [] ) as T; }
@winkyBrain@adminNiochenСпасибо, что мотивировали пойти и собственноручно разобраться в этом! А то бы до последнего в это верил, как дурак
winkyBrain
26.12.2024 19:10Я рекомендовал бы вам прочитать статью про работу AST (на ней работает большинство современных js-фреймворков, типа React, Vue, Svelte и другие)
Тоже странное заявление, ведь AST используется в самих языках программирования) в том числе в JS. Очевидно, что всё, что написано на таком языке, на этапе парсинга превратится в AST. При чём тут фрейвморки - остаётся только гадать)
monochromer
26.12.2024 19:10C AST может работать транспилятор из jsx в js (например, Babel или TypeScript), но не сам React. React Compiler, добавляющий оптимизации, поставляется как плагин к транспилятору.
savostin
26.12.2024 19:10function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(`Count is: ${count}`); }, 1000); return () => clearInterval(timer); }, []); return <button onClick={() => setCount(count + 1)}>Increment</button>; }
И как здесь может помочь useCallback?
Zukomux
26.12.2024 19:10О-хо-хо. А вам в панамку не напихали ещё за описание работы хука? При использовании реакт всегда! создаёт новый экземпляр функции и либо отбрасывает его, если зависимости не поменялись, либо возвращает старую ссылку на функцию.
"On the following renders, React will compare the dependencies with the dependencies you passed during the previous render. If none of the dependencies have changed (compared with Object.is), useCallback will return the same function as before. Otherwise, useCallback will return the function you passed on this render"
adminNiochen
Никто не говорит "функции обратного вызова" - все говорят "колбэк".
Также написана ересь про ненужные создания функций при ререндерах. Юзколбэк нужен чтобы передавать стабильную ссылку на функцию - всё. Но на каждый ререндер вы всё равно создаёте и прокидываете в него новую функцию. Оптимизация будет только если есть дочерние компоненты, обёрнутые memo
winkyBrain
ни дать, ни взять - всё по делу)
donatello2005
В обычном JS - да, но в JSX (с переводом в AST) - новая функция не создаётся при каждом рендере, если зависимости не менялись.
Если коротко, то JSX - это просто формат файла, который к JS имеет очень косвенное отношение. Синтаксис похож на JS, но он не работает как JS. Из него сначала строится абстрактное синтаксическое дерево (AST), потом отдельный механизм в React обрабатывает каждую часть кода отдельно, и на основе этого уже строится JS. Поэтому привычные законы JS с ним не работают, там происходит очень много магии под капотом при компиляции.
Я в комментарии ниже подробнее описал, не буду здесь дублировать.