React Hook-ове — това са функции, които позволяват използването на състояние, ефекти, контекст и други възможности на React във функционални компоненти, без писане на класове. Представени в React 16.8, hook-овете радикално промениха подхода към разработката, заменяйки сложни модели като HOC и render props. Според React, 2024, съществуват 15 вградени hook-а, покриващи всички сценарии за управление на компоненти.
Основни
React Hook-ове — набор от вградени функции в React, които позволяват ‘свързване’ с вътрешните механизми на рамката без създаване на класове. Преди React 16.8 функционалните компоненти можеха само да приемат props и да връщат JSX — всяка сложна логика изискваше класов компонент.
Проблемът на класовите компоненти — разделяне на свързаната логика. Кодът, отнасящ се до една същност (например WebSocket абонамент), беше принудително разделен между componentDidMount, componentDidUpdate и componentWillUnmount. Hook-овете решават това: цялата логика се групира в един useEffect.
Вторият проблем — повторно използване на логика. За повторно използване на логика на състояние между класови компоненти трябваше да се прилагат HOC (компоненти от по-висок ред) или render props — и двата модела създаваха допълнително влагане в дървото на компонентите. Персонализираните hook-ове решават този проблем без влагане.
useState — най-основният hook. Той приема начална стойност и връща tuple: текущата стойност и функция за нейното актуализиране. За разлика от this.setState в класовете, useState не слива обекти — новото състояние напълно замества предишното за конкретно извикване на useState.
import React, { useState } from 'react';
function Form() {
const [text, setText] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit() {
setSubmitting(true);
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ text })
});
setSubmitting(false);
}
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={handleSubmit}
disabled={submitting}>
{submitting ? 'Saving...' : 'Submit'}
</button>
</div>
);
}
Setter-ът на useState може да приеме функция, която получава предишната стойност и връща нова. Функционалната форма е задължителна, когато новото състояние се изчислява от предишното и са възможни групирани извиквания на setState — всяко извикване ще получи актуалната предишна стойност.
useEffect — hook за изпълнение на странични ефекти във функционални компоненти. Той приема два аргумента: функция-ефект и масив от зависимости. React изпълнява ефекта след всеки рендер, но при посочване на зависимости — само когато някоя от тях се промени.
Празен масив от зависимости [] означава, че ефектът се изпълнява веднъж след първия рендер (аналогично на componentDidMount). Връщането на функция от ефекта е cleanup, който се изпълнява при демонтиране (аналогично на componentWillUnmount) и преди повторното изпълнение на ефекта.
import React, { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket(`wss://chat/${roomId}`);
socket.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
// Cleanup - затваряне на socket
return () => socket.close();
}, [roomId]);
return <ul>
{messages.map((msg, i) =>
<li key={i}>{msg.text}</li>
)}
</ul>;
}
Зависимостите на useEffect — са единствената връзка между ефекта и външния свят. Ако в ефекта се използва променлива от props или състояние, тя трябва да бъде в масива от зависимости. Пренебрегването на това правило е източник на грешки с остарели данни (stale closure).
useContext — hook за четене на стойността на контекста. Той приема обекта на контекста, създаден чрез React.createContext, и връща текущата стойност. Ако контекстът се промени, компонентът автоматично се пререндерира с новата стойност.
Преди появата на useContext, достъпът до контекст във функционални компоненти изискваше обвивка в Context.Consumer, което създаваше влагане. useContext напълно елиминира Consumer — достатъчно е да извикате hook-а на всяко място във функционалния компонент.
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('светъл');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button
className={`btn btn-${theme}`}
>
{theme === 'dark' ? 'moon' : '☀️'}
</button>;
}
useContext не е заместител на Redux или глобално състояние. React препоръчва използването на контекст за рядко променящи се стойности — тема, език, оторизация. За често актуализирани данни всеки пререндер ще преизчисли всички потребители на контекста, което може да намали производителността.
useReducer — алтернатива на useState за състояния със сложна логика на актуализация. Той приема reducer функция и начално състояние, връща текущото състояние и dispatch функция. Reducer получава текущото състояние и действие, връща ново състояние — чиста функция без странични ефекти.
useReducer е особено полезен, когато няколко действия (set, reset, increment, addItem) управляват едно състояние. Вместо няколко useState с различни setter-и — един reducer с ясни типове действия. Това прави логиката предвидима и лесна за тестване.
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setStep':
return { ...state, step: action.step };
case 'reset':
return { count: 0, step: 1 };
default:
throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1
});
return (
<>
<p>Count: {state.count}</p>
<button
onClick={() => dispatch({ type: 'increment' })}>+</button>
<button
onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
useRef създава променлив обект, който се запазва между рендерите. Промяната на current не предизвиква пререндер. Основно приложение — достъп до DOM елементи и съхраняване на мутабилни стойности (таймери, предишни стойности, инстанции на библиотеки).
useMemo мемоизира резултата от функция. Преизчислява стойността само когато се промени една от зависимостите. Използвайте за скъпи изчисления — филтриране на големи масиви, сортиране, форматиране на данни. useCallback — специален случай на useMemo за мемоизация на функции.
import React, { useRef, useMemo, useCallback } from 'react';
function SearchPage({ items, query, onItemSelect }) {
const inputRef = useRef(null);
// Автофокус при монтиране
useEffect(() => {
inputRef.current?.focus();
}, []);
// Мемоизирай филтриран резултат
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase()
.includes(query.toLowerCase())
);
}, [items, query]);
// Мемоизирай callback
const handleSelect = useCallback((id) => {
onItemSelect(id);
}, [onItemSelect]);
return (
<div>
<input ref={inputRef}
value={query}
onChange={e => setQuery(e.target.value)}
/>
<ul>
{filtered.map(item =>
<li key={item.id}>
<button
onClick={() => handleSelect(item.id)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
Важно правило: useRef, useMemo и useCallback — инструменти за оптимизация, а не задължителни конструкции. Не обвивайте всичко в useMemo — първо измерете производителността. Преждевременната оптимизация усложнява кода. React рендерира компоненти бързо; useMemo е необходим само когато изчисленията са наистина скъпи (O(n²) и повече).
React въведе две строги правила за hook-ове. Първо: извиквайте hook-ове само на най-горно ниво — не вътре в условия, цикли или вложени функции. Второ: извиквайте hook-ове само от React функционални компоненти или персонализирани hook-ове. ESLint плъгинът eslint-plugin-react-hooks автоматично проверява и двете правила.
Най-честата грешка — нарушаване на реда на извикване на hook-овете. React разчита на реда на извикване на hook-овете между рендерите. Ако hook се намира вътре в условие и не е бил извикан в един от рендерите, всички последващи hook-ове се изместват, причинявайки грешки.
| Грешка | Какво се случва | Решение |
|---|---|---|
| Hook в условие | Редът на hook-овете се нарушава | Преместете логиката на условието вътре в hook-а |
| Stale closure | Ефектът използва стара стойност | Добавете всички зависимости в масива |
| Безкраен цикъл | useEffect без зависимости променя състояние | Посочете зависимости или използвайте useReducer |
| Промяна на ref в рендер | Страничен ефект в тялото на функция | Преместете мутациите в useEffect |
Втората честа грешка — stale closure в useEffect. Ако в ефекта се използва променлива, която не е посочена в зависимостите, ефектът ‘улавя’ нейната стойност в момента на създаване на затварянето. Решение — винаги включвайте всички използвани променливи в масива от зависимости. Ако зависимостите са твърде много, разделете ефекта на няколко части.
Често задавани въпроси
React разчита на реда на извикване на hook-овете между рендерите. Ако hook е извикан в условие и не е работил в един от рендерите, всички последващи hook-ове ‘се плъзгат’ — тяхното състояние се смесва. Правилото на ESLint hooks/exhaustive-deps предупреждава за това.
useLayoutEffect е синхронен и се изпълнява преди браузърът да нарисува промените. useEffect е асинхронен — след рисуването. Използвайте useLayoutEffect само когато трябва да измерите или промените DOM, преди потребителят да види промените (позиция на popover, възстановяване на скрол).
Използвайте AbortController вътре в useEffect. Създайте controller, предайте signal на fetch и в cleanup функцията извикайте controller.abort(). За стари браузъри е достатъчен флаг cancelled: return () => { cancelled = true; } с проверка преди setState.
Персонализиран hook — JavaScript функция с префикс use, която използва вградени hook-ове. Създавайте го, когато една и съща логика с hook-ове се повтаря в два или повече компонента. Примери: useDebounce, useLocalStorage, useMediaQuery. Персонализираните hook-ове заменят HOC и render props.
В Strict Mode React умишлено монтира и демонтира компонента два пъти в режим на разработка, за да открие грешки с неизчистени ефекти. Ако cleanup е написан правилно, двойното извикване не причинява проблеми. В производствен режим това не се случва.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също