Яку проблему вирішують hooks в React?
React hooks - це функції, які дозволяють функціональним компонентам мати стан, побічні ефекти та інші можливості React без написання класів.
Теорія
TL;DR
- До React 16.8 тільки класові компоненти могли мати стан і lifecycle методи
- Щоб ділитися логікою між класовими компонентами, використовували HOC або render props, що утворювало глибоку вкладеність
- Lifecycle методи розкидали пов'язаний код по різних місцях: setup у
componentDidMount, очищення уcomponentWillUnmount thisу класах постійно провокує помилки- Hooks вирішують всі три проблеми через звичайні функції
Клас проти хука: порівняння
// Клас: setup і cleanup у різних lifecycle методах
class ChatRoom extends React.Component {
componentDidMount() { this.socket = openSocket(this.props.roomId); }
componentWillUnmount() { this.socket.close(); }
}
// Хук: один ефект, обидва разом
function ChatRoom({ roomId }) {
useEffect(() => {
const socket = openSocket(roomId);
return () => socket.close(); // очищення поруч із запуском
}, [roomId]);
}Клас розбиває одну логіку на два методи. Хук тримає її в одному місці. В цьому вся суть.
Чому класові компоненти не масштабувалися
Класовий компонент із середньою складністю розкидав логіку по чотирьох місцях: стан у конструкторі, підписки у componentDidMount, реакції на зміни пропсів у componentDidUpdate, очищення у componentWillUnmount. Чотири місця, одна логіка.
Але головна проблема - повторне використання. Якщо двом компонентам потрібна однакова логіка підписки, чистого рішення не було. Використовували HOC або render props. Обидва варіанти робочі. Але накласти кілька HOC, і React DevTools показує Connect(WithAuth(WithTheme(MyComponent))). Це і є wrapper hell.
Коли я бачив кодові бази після міграції з класів на hooks, файли компонентів ставали коротшими. Але важливіше інше: логіку стає видно. Читаєш useEffect і за кілька рядків бачиш повний lifecycle цього ефекту.
Custom hooks: де проблема вирішується остаточно
useState і useEffect корисні, але custom hooks (власні хуки) - це місце, де проблеми з повторним використанням вирішуються по-справжньому. Custom hook - це функція, назва якої починається з use і яка всередині викликає інші hooks. Виносиш логіку підписки у useSubscription, стан форми у useForm, запити до API у useFetch. Будь-який компонент може це викликати. Без HOC, без вкладеності, без зайвих шарів у дереві.
Типові помилки
Відсутній масив залежностей:
useEffect(() => {
fetchUser(userId);
}); // запускається після кожного рендеру
useEffect(() => {
fetchUser(userId);
}, [userId]); // запускається тільки при зміні userIdЗабуте очищення:
// Баг: listener накопичується після кожного рендеру
useEffect(() => {
window.addEventListener('keydown', handleKey);
}, []);
// Правильно
useEffect(() => {
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, []);Читання стану одразу після оновлення:
setCount(count + 1);
console.log(count); // старе значення - React батчує оновленняОновлення стану асинхронне. Нове значення буде доступне тільки після наступного рендеру.
Питання на співбесіді
Q: Чому не можна викликати hooks у умовах або циклах?
A: React відстежує hooks за порядком виклику. Якщо хук пропустити в одному рендері, порядок зміщується і React читає неправильний стан для кожного наступного хука. Правило підтримує стабільний порядок між рендерами.
Q: Що таке custom hook?
A: Функція, назва якої починається з use і яка всередині викликає інші hooks. Дозволяє виносити логіку стану в окрему функцію без змін у дереві компонентів.
Q: Що використовували до hooks для спільного використання логіки?
A: HOC і render props. Обидва варіанти робочі, але додають зайві шари компонентів. Custom hook вирішує те саме через звичайний виклик функції.
Q: useEffect з [] повністю замінює componentDidMount?
A: Майже. Обидва запускаються після першого рендеру. Але useEffect запускається після відмальовки браузером, а componentDidMount - після оновлення DOM, до відмальовки. Для вимірювань layout краще підходить useLayoutEffect.
Приклади
Лічильник: клас проти хука
// Клас: конструктор, bind і this скрізь
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.increment = this.increment.bind(this); // легко забути
}
increment() {
this.setState({ count: this.state.count + 1 });
}
render() {
return <button onClick={this.increment}>{this.state.count}</button>;
}
}
// Хук: та сама поведінка, без зайвого
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Клас потребує конструктора, виклику bind і this скрізь. Функціональний компонент з useState робить те саме в два рядки.
Custom hook: повторне використання логіки запитів
// Логіка запиту в одному місці - будь-який компонент може використати
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(url)
.then(res => res.json())
.then(result => { if (!cancelled) setData(result); })
.catch(err => { if (!cancelled) setError(err); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; }; // запобігає оновленню після unmount
}, [url]);
return { data, loading, error };
}
// Будь-який компонент отримує стан запиту в один рядок
function UserProfile({ userId }) {
const { data: user, loading } = useFetch(`/api/users/${userId}`);
if (loading) return <span>Завантаження...</span>;
return <h1>{user.name}</h1>;
}Логіка запиту живе в одному місці й тестується один раз. До hooks команди або дублювали цей код у кожному компоненті, або загортали в HOC. Custom hook вирішує це без жодної вкладеності.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.