Какие Hooks позволяют оптимизировать React?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
React Hooks для оптимизации производительности
React hooks — это мощный инструмент для оптимизации компонентов. Хотя я backend разработчик, работаю с React в проектах и знаю, какие hooks помогают избежать ненужных рендеров и улучшить производительность.
1. useMemo — Мемоизация вычислений
Проблема: Дорогие вычисления при каждом рендере
// ❌ Плохо: calculateExpensiveValue вызывается при КАЖДОМ рендере
function UserProfile({ users }) {
const expensiveValue = calculateExpensiveValue(users);
return <div>{expensiveValue}</div>;
}
// При 10 000 пользователях это может занять 500ms на каждый рендер!
Решение: useMemo
import { useMemo } from 'react';
function UserProfile({ users }) {
const expensiveValue = useMemo(() => {
console.log('Calculating...');
return calculateExpensiveValue(users);
}, [users]); // Пересчитывается только если users изменилась
return <div>{expensiveValue}</div>;
}
// Log будет выведен только когда users изменится
// Не при каждом рендере!
Когда использовать useMemo:
- Дорогие вычисления (сортировка, фильтрация больших массивов)
- Сложные трансформации данных
- Вычисления требующие O(n²) или больше
const sortedUsers = useMemo(() => {
// O(n log n) — дорого
return users.sort((a, b) => a.name.localeCompare(b.name));
}, [users]);
const filteredAndGrouped = useMemo(() => {
// Сложная фильтрация
return users
.filter(u => u.active)
.reduce((acc, u) => {
acc[u.department] = [...(acc[u.department] || []), u];
return acc;
}, {});
}, [users]);
2. useCallback — Мемоизация функций
Проблема: Новая функция при каждом рендере
// ❌ Плохо: handleClick создается заново при каждом рендере
function Button() {
const handleClick = () => {
console.log('Clicked');
};
return <button onClick={handleClick}>Click me</button>;
}
// Если Button имеет дочерние компоненты со своей оптимизацией
// они будут ре-рендериться из-за новой функции
Решение: useCallback
import { useCallback } from 'react';
function Button() {
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Функция никогда не пересоздается
return <button onClick={handleClick}>Click me</button>;
}
// Или с зависимостями
function UserForm({ userId }) {
const handleSubmit = useCallback(async (data) => {
await api.updateUser(userId, data);
}, [userId]); // Пересоздается только если userId изменится
return <form onSubmit={handleSubmit}>...</form>;
}
Когда использовать useCallback:
- Передача callback в optimized child компоненты (React.memo)
- Использование функции в зависимостях других hooks
- Event handlers, которые передаются children
const List = React.memo(function List({ items, onItemClick }) {
return items.map(item => (
<Item key={item.id} item={item} onClick={onItemClick} />
));
});
function Parent() {
const handleItemClick = useCallback((id) => {
console.log('Clicked:', id);
}, []);
return <List items={items} onItemClick={handleItemClick} />;
}
// Без useCallback List пересчитывается на каждый рендер Parent!
3. React.memo — Мемоизация компонентов
Проблема: Компонент рендерится даже если props не изменились
// ❌ Плохо: UserCard рендерится при каждом рендере Parent
function UserCard({ user }) {
console.log('UserCard render');
return <div>{user.name}</div>;
}
function Parent({ users }) {
return users.map(user => <UserCard key={user.id} user={user} />);
}
// Если в Parent есть другой state — все UserCard ре-рендерятся!
Решение: React.memo
// ✅ Хорошо: рендерится только если user prop изменился
const UserCard = React.memo(function UserCard({ user }) {
console.log('UserCard render');
return <div>{user.name}</div>;
});
function Parent({ users }) {
return users.map(user => <UserCard key={user.id} user={user} />);
}
// Теперь UserCard#1 не ре-рендерится если изменился UserCard#2!
Custom comparison:
const UserCard = React.memo(
function UserCard({ user, onUpdate }) {
return <div onClick={() => onUpdate(user.id)}>{user.name}</div>;
},
(prevProps, nextProps) => {
// Вернуть true если props "равны" (не ре-рендерить)
return prevProps.user.id === nextProps.user.id &&
prevProps.onUpdate === nextProps.onUpdate;
}
);
4. useReducer — Оптимизированное state management
Проблема: useState с множеством состояний
// ❌ Плохо: множество useState
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [address, setAddress] = useState('');
// ... 10 more fields
// Каждое изменение вызывает ре-рендер всего компонента
}
Решение: useReducer
const initialState = {
name: '',
email: '',
phone: '',
address: ''
};
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return initialState;
default:
return state;
}
}
function Form() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (field) => (e) => {
dispatch({ type: 'UPDATE_FIELD', field, value: e.target.value });
};
return (
<form>
<input value={state.name} onChange={handleChange('name')} />
<input value={state.email} onChange={handleChange('email')} />
</form>
);
}
Преимущества:
- Одна функция dispatch вместо множества setState
- Логика state управления в одном месте
- Легче тестировать (reducer — pure function)
5. useContext — Избегание prop drilling
Проблема: Prop drilling (пробрасывание пропсов через множество компонентов)
// ❌ Плохо: theme пробрасывается через все компоненты
function App() {
const [theme, setTheme] = useState('light');
return <Navbar theme={theme} setTheme={setTheme} />;
}
function Navbar({ theme, setTheme }) {
return <Menu theme={theme} setTheme={setTheme} />;
}
function Menu({ theme, setTheme }) {
return <Button theme={theme} onClick={() => setTheme('dark')} />;
}
Решение: useContext
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Navbar />
</ThemeContext.Provider>
);
}
function Button() {
const { theme, setTheme } = useContext(ThemeContext);
return <button onClick={() => setTheme('dark')}>{theme}</button>;
}
// Navbar и Menu не нужно знать про theme!
Оптимизация useContext:
// ✅ Разделить контексты по изменяемости
const ThemeContext = createContext(); // Редко меняется
const UserContext = createContext(); // Часто меняется
// Иначе все компоненты ре-рендерятся при изменении
6. useEffect с зависимостями — Контроль side effects
Оптимизация:
// ❌ Плохо: выполняется при каждом рендере
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
api.getUser(userId).then(setUser);
// Без зависимостей — бесконечные запросы!
});
return <div>{user?.name}</div>;
}
// ✅ Хорошо: выполняется только когда userId изменится
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
api.getUser(userId).then(setUser);
}, [userId]); // Зависимость!
return <div>{user?.name}</div>;
}
7. useTransition — Оптимизация медленных обновлений
Проблема: Медленный поиск блокирует UI
// ❌ Плохо: input lag при поиске в большом списке
function SearchUsers({ users }) {
const [search, setSearch] = useState('');
const filtered = users.filter(u =>
u.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<input
value={search}
onChange={e => setSearch(e.target.value)}
// Это блокирует UI на 100ms+!
/>
{filtered.map(u => <div key={u.id}>{u.name}</div>)}
</>
);
}
Решение: useTransition
import { useTransition } from 'react';
function SearchUsers({ users }) {
const [search, setSearch] = useState('');
const [isPending, startTransition] = useTransition();
const filtered = users.filter(u =>
u.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<input
value={search}
onChange={e => {
setSearch(e.target.value);
startTransition(() => {
// Это обновление менее приоритетно
// UI остается отзывчивым
});
}}
/>
{isPending && <p>Searching...</p>}
{filtered.map(u => <div key={u.id}>{u.name}</div>)}
</>
);
}
8. useDeferredValue — Отложенное обновление значения
function App() {
const [input, setInput] = useState('');
const deferredInput = useDeferredValue(input);
return (
<>
<input value={input} onChange={e => setInput(e.target.value)} />
<ExpensiveList query={deferredInput} />
</>
);
}
// input обновляется сразу (UI responsive)
// ExpensiveList обновляется с задержкой (менее приоритетно)
Правило: Когда оптимизировать
// Профилируй ДО оптимизации!
function App() {
return <Profiler id="app" onRender={onRender}>
{/* Компоненты */}
</Profiler>;
}
const onRender = (
id, // Идентификатор профайлера
phase, // "mount" или "update"
actualDuration, // Время рендера
baseDuration, // Время без мемоизации
startTime, // Время начала
commitTime // Время завершения
) => {
console.log(`${id} took ${actualDuration}ms`);
};
Best Practices
- Не оптимизируй без данных — используй React DevTools Profiler
- Не überoptimize — memoization сама по себе имеет стоимость
- Правильно настраивай зависимости — неправильные зависимости опаснее отсутствия мемоизации
- Используй key в lists — это базовая оптимизация
- Разделяй state — разные части state менять отдельно
Оптимизация React — это баланс между производительностью и сложностью кода. Начни с простого, профилируй, потом оптимизируй.