Skip to main content

Яку проблему вирішують hooks в React?

React hooks - це функції, які дозволяють функціональним компонентам мати стан, побічні ефекти та інші можливості React без написання класів.

Теорія

TL;DR

  • До React 16.8 тільки класові компоненти могли мати стан і lifecycle методи
  • Щоб ділитися логікою між класовими компонентами, використовували HOC або render props, що утворювало глибоку вкладеність
  • Lifecycle методи розкидали пов'язаний код по різних місцях: setup у componentDidMount, очищення у componentWillUnmount
  • this у класах постійно провокує помилки
  • Hooks вирішують всі три проблеми через звичайні функції

Клас проти хука: порівняння

jsx
// Клас: 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, без вкладеності, без зайвих шарів у дереві.

Типові помилки

Відсутній масив залежностей:

jsx
useEffect(() => { fetchUser(userId); }); // запускається після кожного рендеру useEffect(() => { fetchUser(userId); }, [userId]); // запускається тільки при зміні userId

Забуте очищення:

jsx
// Баг: listener накопичується після кожного рендеру useEffect(() => { window.addEventListener('keydown', handleKey); }, []); // Правильно useEffect(() => { window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, []);

Читання стану одразу після оновлення:

jsx
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.

Приклади

Лічильник: клас проти хука

jsx
// Клас: конструктор, 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: повторне використання логіки запитів

jsx
// Логіка запиту в одному місці - будь-який компонент може використати 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 вирішує це без жодної вкладеності.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?