Зачем нужны стабильные ссылки на функции?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Стабильные ссылки на функции в React
Стабильная ссылка на функцию — это функция, которая не меняется между ре-рендерингами компонента. Это важный момент в React для оптимизации производительности и правильного поведения эффектов.
Проблема: нестабильные функции
По умолчанию в React функция создаётся заново при каждом ре-рендеринге:
function Parent() {
// Эта функция создаётся заново каждый ре-рендеринг!
const handleClick = () => {
console.log('clicked');
};
return <Child onClick={handleClick} />;
}
function Child({ onClick }: { onClick: () => void }) {
useEffect(() => {
// ПРОБЛЕМА: эффект запускается каждый ре-рендеринг родителя
// потому что onClick функция новая!
console.log('Effect runs');
}, [onClick]); // onClick меняется -> эффект запускается
return <button onClick={onClick}>Click me</button>;
}
Что происходит:
- Parent рендерится -> создаёт новую
handleClick - Передаёт её в Child
- Child видит новую функцию -> запускает useEffect
- useEffect может быть дорогой операцией (запрос на сервер, подписка на события)
В результате — бесконечный цикл обновлений.
Решение 1: useCallback
useCallback мемоизирует функцию, возвращая ту же ссылку, пока зависимости не изменились:
function Parent() {
const [count, setCount] = useState(0);
// Функция стабильна — один раз создана, переиспользуется
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // Зависимостей нет — функция создаётся один раз
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
);
}
function Child({ onClick }: { onClick: () => void }) {
useEffect(() => {
console.log('Effect runs only once'); // Запускается один раз!
}, [onClick]); // onClick стабильна
return <button onClick={onClick}>Click me</button>;
}
С зависимостями:
function Parent() {
const [userId, setUserId] = useState(1);
// Функция зависит от userId
// Создаётся заново, когда userId меняется
const handleLoadUser = useCallback(() => {
fetch(`/api/users/${userId}`); // Использует текущий userId
}, [userId]); // userId в зависимостях
return (
<>
<Child onLoadUser={handleLoadUser} />
<button onClick={() => setUserId(userId + 1)}>Next user</button>
</>
);
}
Решение 2: Встроенные функции (event handlers)
Для обработчиков событий в JSX можно использовать встроенные функции — React оптимизирует их:
function MyComponent() {
const [count, setCount] = useState(0);
// React оптимизирует это — функция может пересоздаваться,
// но обработчик остаётся стабильным для Child
return (
<Child onClick={() => setCount(count + 1)} />
);
}
Решение 3: Классовые компоненты и методы
В классах методы по умолчанию стабильны:
class MyComponent extends React.Component<any> {
handleClick = () => { // Arrow function property — стабильна
console.log('clicked');
};
render() {
return <Child onClick={this.handleClick} />; // Всегда одна и та же ссылка
}
}
Без стрелочной функции пришлось бы вручную привязывать:
class MyComponent extends React.Component<any> {
constructor(props: any) {
super(props);
this.handleClick = this.handleClick.bind(this); // Привязка в конструкторе
}
handleClick() {
console.log('clicked');
}
render() {
return <Child onClick={this.handleClick} />;
}
}
Когда стабильность ссылок критична
1. Оптимизация перерисовок с React.memo
const Button = React.memo(function Button({
onClick,
label,
}: {
onClick: () => void;
label: string;
}) {
console.log('Button rendered'); // Сколько раз запускается?
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// БЕЗ useCallback
const handleOld = () => console.log('old');
// С useCallback
const handleNew = useCallback(() => {
console.log('new');
}, []);
return (
<>
<Button onClick={handleOld} label="Old" />
{/* "Button rendered" выведется каждый раз при ре-рендере Parent */}
<Button onClick={handleNew} label="New" />
{/* "Button rendered" выведется только один раз! */}
<button onClick={() => setCount(count + 1)}>Trigger re-render</button>
</>
);
}
2. Зависимости в useEffect и других хуках
function DataFetcher({ userId }: { userId: number }) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
// Этот fetchData функция нужна для useEffect
const fetchData = useCallback(async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
}
}, [userId]); // Зависит от userId
useEffect(() => {
fetchData(); // Запускается, когда fetchData меняется (когда меняется userId)
}, [fetchData]); // fetchData в зависимостях
return <>...</>;
}
3. Подписки на события
function DataFetcher() {
const [search, setSearch] = useState('');
// БЕЗ useCallback — функция пересоздаётся каждый ре-рендеринг
const handleSearch = (query: string) => {
fetch(`/api/search?q=${query}`);
};
useEffect(() => {
const timeout = setTimeout(() => handleSearch(search), 500);
return () => clearTimeout(timeout);
}, [search, handleSearch]); // handleSearch в зависимостях!
// -> При каждом ре-рендере timeout переустанавливается -> плохо
return <input onChange={(e) => setSearch(e.target.value)} />;
}
// Лучше:
function DataFetcher() {
const [search, setSearch] = useState('');
const handleSearch = useCallback((query: string) => {
fetch(`/api/search?q=${query}`);
}, []); // Зависит только от пустого массива
useEffect(() => {
const timeout = setTimeout(() => handleSearch(search), 500);
return () => clearTimeout(timeout);
}, [search, handleSearch]); // handleSearch стабильна -> useEffect запускается только при изменении search
return <input onChange={(e) => setSearch(e.target.value)} />;
}
Производительность: когда useCallback помогает, а когда нет
Помогает useCallback
// Передача мемоизированного компонента
const ExpensiveChild = React.memo(({ onClick }) => {
// Дорогой рендеринг — расчёты, большой список
const items = Array.from({ length: 1000 }, (_, i) => i);
return (
<div>
{items.map(item => <div key={item}>{item}</div>)}
<button onClick={onClick}>Click</button>
</div>
);
});
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // useCallback предотвращает ненужный рендеринг ExpensiveChild
return <ExpensiveChild onClick={handleClick} />;
}
Может быть не нужен useCallback
// Простой компонент
function SimpleChild({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>Click</button>; // Дешёвый рендеринг
}
function Parent() {
// useCallback тут можно опустить — рендеринг SimpleChild дешёвый
const handleClick = () => {
console.log('clicked');
};
return <SimpleChild onClick={handleClick} />;
}
Best Practices
1. Используй useCallback для дорогих компонентов
const ExpensiveChart = React.memo(({ onDataUpdate }: any) => {
// Дорогой рендеринг
return <div>Chart</div>;
});
function Dashboard() {
const handleDataUpdate = useCallback(() => {
// Запрос на сервер
}, []);
return <ExpensiveChart onDataUpdate={handleDataUpdate} />;
}
2. Не переусложняй
function SimpleForm() {
// useCallback не нужен — простой компонент
return (
<input
onChange={(e) => {
console.log(e.target.value);
}}
/>
);
}
3. Следи за зависимостями
const handler = useCallback(() => {
console.log(userId); // userId должна быть в зависимостях!
}, [userId]); // ПРАВИЛЬНО
// const handler = useCallback(() => {
// console.log(userId);
// }, []); // НЕПРАВИЛЬНО — будет старое значение
Альтернатива: useMemo для объектов
Стабильность нужна не только функциям, но и объектам:
function Parent() {
// Объект пересоздаётся каждый ре-рендеринг
const config = { fontSize: 14, color: 'red' };
return <Child config={config} />; // Child перерендерится
}
// Лучше:
function Parent() {
const config = useMemo(() => ({
fontSize: 14,
color: 'red',
}), []); // Стабильный объект
return <Child config={config} />; // Child не перерендерится
}
Заключение
Стабильные ссылки нужны для:
- Оптимизации React.memo — предотвратить лишние ре-рендеры
- Зависимостей хуков — useEffect, useMemo, useCallback сами
- Предсказуемого поведения — избежать бесконечных циклов
- Правильной работы с подписками — не переустанавливать слушатели
Как обеспечить стабильность:
useCallbackдля функцийuseMemoдля объектов и массивов- Классовые компоненты (методы стабильны по умолчанию)
- Event handlers в JSX (React оптимизирует автоматически)