React Hooks
Разделы:
Хук | Описание |
useState | Позволяет работать с состоянием компонента и управлять его рендером |
useEffect | Выполняет сайд-эффекты, не блокируя рендер |
useContext | Позволяет обмениваться данными между компонентами на разных уровнях без передачи параметров |
useReducer | Позволяет вынести бизнес-логику компонента в редьюсер и управлять ею через диспатчи, а также организовывает общее хранилище данных в initialState |
useCallback | Возвращает ссылку на функцию (кеширует функцию), при повторном рендере ссылка не изменится, если не были переданы изменённые данные в массив зависимостей |
useMemo | Принимает функцию, которая возвращает что-либо, выполняет функцию при первом рендере и кеширует результат, при повторном вызове возвращает мемоизированное значение. Если изменился массив зависимостей, то выполнит функцию заново. |
useRef | Принимает данные любого типа, возвращает объект с свойством current. При рендерах компонента не перезаписывает ref объект и изменение current не влияют на рендер, что позволяет хранить промежуточные данные. |
useImperativeHandle | Позволяет получить ссылку на функциональный компонент (можно передать объект со стейтом дочерного компонента и управлять им из родительского). |
useLayoutEffect | выполняет сайд-эффекты, не блокируя рендер, но блокируя отображение |
useDebugValue | используется для отображения метки (помечаем вкладку статусом) пользовательских хуков в React DevTools |
useId | создаёт уникальный идентификатор, связанный с этим конкретным useId вызовом в этом конкретном компоненте |
useDeferredValue | Позволяет кешировать значения, возвращать старые в отсутствие новых, тем самым откладывает рендер на потом |
useTransition | Позволяет вам обновлять состояние, не блокируя пользовательский интерфейс, а также отслеживать статус выполнения startTransition |
useState
Позволяет создать переменную состояния и метод для её изменения.
Когда мы изменим значение переменной состояния, то компонент будет перерендерен.
Значение переменной будет сохранено и при повторном рендере не пропадёт.
const [count, setCount] = useState(0);
const [isActive, setIsActive] = useState(false);
const [lists, setLists] = useState([]);
Данные в стейте могут быть любыми: объекты, функции, массивы, примитивы и тп.
function Practice() {
const [isActive, setIsActive] = useState(false);
const handleButtonClick = () => {
setIsActive(!isActive);
}
return (
<div className='practice'>
<button
className='practice-button'
onClick={handleButtonClick}>Переключатель</button>
{
isActive ? <p className='practice-text'>Текстовое сообщение</p> :
<Link className='practice-link' to='/'>На главную</Link>
}
</div>
);
}
useEffect
Первым аргументом принимает функцию, вторым аргументом принимает массив зависимостей.
Функция callback
будет вызвана при монтировании компонента и при последующем рендере. Повторный вызов при рендере можно отменить, передав пустой массив зависимостей, либо не изменять переданные в него данные. Функция callback
может возвращать либо другую функцию либо ничего. Если будет возвращена функция, то она будет вызвана при размонтировании компонента или при следующем рендере, но с состоянием предыдущего рендера.
Чаще всего используется, когда нужно выполнить асинхронные или ресурсоёмкие действия,
при этом не блокировать рендер компонента.
function Practice(props) {
const { url } = props;
const [profile, setProfile] = useState('');
useEffect(() => {
const controller = new AbortController();
//url = 'https://api.github.com/users/coder1x';
(async () => {
const response = await fetch(url, {
signal: controller.signal
});
if (response.ok && response.status === 200) {
setProfile(await response.text());
}
})();
return () => {
controller.abort();
}
}, [url]);
return (
<pre className='practice'>
{profile}
</pre>
);
}
useContext
Принимает объект, который возвращает createContext
. Возвращает данные, которые
были переданы в value Provider
или createContext
(как значение по умолчанию, если не был указан Provider
на верхнем уровне), также заставляет компонент перерендерится если данные изменились, для этого нужно использовать useState
и менять данные в параметре value
у Provider
.
Используется там, где необходима глубокая передача данных в дерево, оптимизация повторного рендеринга при передаче объектов и функций.
const Context = createContext({ text: '' });
function Practice() {
const [data, setData] = useState({ text: 'backend' });
const handleButtonClick = () => {
setData({ text: 'frontend' });
};
return (
<Context.Provider value={data}>
<button onClick={handleButtonClick}>Изменить данные</button>
<div className='practice'>
<Text />
</div>
</Context.Provider>
);
}
function Text() {
const data = useContext(Context);
return <p>{data.text}</p>;
}
// сначала на экране выведется надпись
// backend, а после нажатия кнопки она сменится на frontend
useReducer
Первым аргументом принимает функцию reducer
, вторым initialState
. Есть ещё третий,
необязательный аргумент, которым является функция init (initialState)
, она вычисляет, каким будет начальное состояние, если её нет то за начальное состояние будет взят initialState
.
Возвращает массив [state, dispatch]
, где state
— это текущее состояние, а dispatch
— это функция, при вызове которой мы можем менять это состояние.
useReducer
— это аналог Redux
, он позволяет создать общее хранилище с бизнес логикой.
Для того, чтобы компоненты перерендеривались и были подписаны на изменение state
, нужно использовать useReducer
в связке с useContext
.
const Context = createContext({});
const initialState = {
count: 0,
status: '',
};
function reducer(state, action) {
const { type, payload } = action;
const { count, status } = state;
switch (type) {
case 'DECREMENTED':
return {
count: count ? count - 1 : 0,
status: count ? payload : 'Ноль',
}
case 'INCREMENTED':
return {
count: count + 1,
status: payload,
}
default:
return state;
}
}
function Practice() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ dispatch, state }}>
<div className='practice'>
<Calc />
</div>
</Context.Provider>
);
}
function Calc() {
const { dispatch, state } = useContext(Context);
const handleDecrementedClick = () => {
dispatch({
type: 'DECREMENTED',
payload: 'уменьшил на единицу'
});
}
const handleIncrementedClick = () => {
dispatch({
type: 'INCREMENTED',
payload: 'увеличил на единицу'
});
}
return (
<>
<p>Счётчик: {state.count}</p>
<p>Статус: {state.status}</p>
<button onClick={handleIncrementedlick}>Увеличить</button>
<button onClick={handleDecrementedClick}>Уменьшить</button>
</>
);
}
useCallback
Возвращает ссылку на функцию (кеширует функцию), при повторном рендере ссылка не изменится, если не были переданы изменённые данные в массив зависимостей.
Обычно используется, когда мы передаём дочерним компонентам callback
функцию в
виде параметров, если мы это сделаем без useCallback
, то функция, объявленная в родительском компоненте будет вновь создана при следующем рендере, что изменит на неё ссылку и заставит переотрисоваться дочерний компонент.
const Test = memo((props) => {
return (
<button onClick={props.callback}>Значение по умолчанию</button>
);
});
function Practice(props) {
const { num } = props;
const [count, setCount] = useState(1);
const handleTestClick = useCallback(() => {
setCount(num);
}, [num]);
return (
<div className='practice'>
<p>Счётчик: {count}</p>
<button onClick={() => setCount(count + 1)}>Прибавить</button>
<Test callback={handleTestClick} />
</div>
);
}
При первом рендере компонента Practice
также будет отрендерен и Test
, но при всех последующих рендерах Practice
, Test
не будет рендериться, потому что параметры этого компонента не меняются. Если мы уберём useCallback
, то при каждом рендере Practice
будет создаваться новая ссылка на новую функцию и, так как у Test
будет каждый раз разный параметр, он будет переотрисовываться.
useMemo
Принимает функцию, которая возвращает что-либо, второй аргумент — массив зависимостей. Возвращает мемоизированное значение.
При первом рендере функция в useMemo
будет выполнена, а её результат закеширован (если у функции есть аргументы, то нужно указать значения по умолчанию). Когда произойдёт повторный рендер, useMemo
проверит, что аргументы, функция и массив зависимостей не изменились и тогда она просто вернёт результат и не будет выполнять функцию заново.
function Practice(props) {
const { num } = props;
const [count, setCount] = useState(1);
const numbers = useMemo((lists = []) => {
for (let i = 0; i < 1000; i++) {
lists.push(num);
}
return lists;
}, [num]);
return (
<div className='practice'>
<p>Числа: {JSON.stringify(numbers)}</p>
<button onClick={() => setCount(count + 1)}>Изменить стейт</button>
</div>
);
}
При первом рендере компонента Practice
, useMemo
выполнит callback
функцию, закеширует результат и вернёт его переменной numbers
в виде массива данных размерностью от 0 до 1000.
При клике на кнопку [Изменить стейт] мы заставляем компонент Practice
перерендериться, при этом callback
функция в useMemo
не будет выполнена, так как данные в массиве зависимостей не были изменены, а значит выполнение переданной функции будет иметь такой же результат, поэтому значения будут взяты из кеша — что сильно увеличивает производительность при повторных рендерах.
useRef
Принимает данные любого типа, возвращает объект с свойством current.
Созданный объект через useRef
можно передать в свойство Jsx элемента ref={inputRef}
, при этом в свойство current
будет записана ссылка на DOM-элемент (только после выполнения рендера) на котором мы, к примеру, сможем отловить или вызвать события фокуса ( inputRef.current.focus();
), узнать размеры элемента и т.п.
Не записывайте и не читайте ref.current
во время рендеринга, за исключением инициализации. Это делает поведение вашего компонента непредсказуемым.
function Practice() {
const [text, setText] = useState('');
const inputRef = useRef(null);
const dataRef = useRef(0);
const handleInputChange = (event) => {
const inputText = event.target.value;
setText(inputText);
dataRef.current = inputText.length;
}
useEffect(() => {
inputRef.current.focus();
}, [inputRef]);
return (
<div className='practice'>
<p>Количество знаков: {dataRef.current}</p>
<label>Введите текст:
<input ref={inputRef} onChange={handleInputChange} value={text} type='text' />
</label>
</div>
);
}
inputRef
— передаём jsx элементу, он записывает в свойство current
ссылку на DOM элемент, благодаря этому мы вызываем экшен фокуса на элементе, когда он отрисовывается в первый раз.dataRef
— демонстрирует возможность хранения и изменения любых данных, которые не будут переназначены при повторных рендерах. Изменение значения в current
не влечёт за собой перерисовку компонента, поэтому не стоит использовать это в jsx элементах. Для отрисовки значений лучше использовать useState
, а вариант с reference
необходим, к примеру, когда мы создаём булевый флаг, который может повлиять на условные проверки или массив зависимостей хуков, или когда мы создаём объект, хранящий данные о внутренних расчётах компонента, которые нужно аккумулировать, изменять на протяжении жизни компонента ,а после размонтирования удалить.useImperativeHandle
Для начала разберёмся, как работает forwardRef
, потому что они используются в паре с хуком useImperativeHandle
. forwardRef
позволяет получить ссылку на HTML элемент в функциональном компоненте.
Пример использования:
const CustomInput = forwardRef((props, ref) => {
const [text, setText] = useState('');
const handleInputChange = (event) => {
setText(event.target.value);
}
return (
<label>Введите текст:
<input ref={ref} onChange={handleInputChange} value={text} type='text' />
</label>
);
});
function Practice() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, [inputRef]);
return (
<div className='practice'>
<CustomInput ref={inputRef} />
</div>
);
}
Как видно из примера, CustomInput
получил новое свойство ref
, куда мы можем передать inputRef
и повлиять на HTML-элемент компонента из родительского.
Теперь рассмотрим вариант, как с помощью useImperativeHandle
можно получить ссылку на функциональный компонент.
Принимает три параметра:
ref
, который попадает в функциональный компонент извне- функцию, которая возвращает объект, который будет записан в
ref
- массив зависимостей
const Test = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => {
return { setCount };
})
return (
<p>Количество знаков: {count}</p>
);
});
function Practice() {
const [text, setText] = useState('');
const inputRef = useRef(null);
const dataRef = useRef(null);
const handleInputChange = (event) => {
const inputText = event.target.value;
setText(inputText);
dataRef.current.setCount(inputText.length);
}
useEffect(() => {
inputRef.current.focus();
}, [inputRef]);
return (
<div className='practice'>
<Test ref={dataRef} />
<label>Введите текст:
<input ref={inputRef} onChange={handleInputChange} value={text} type='text' />
</label>
</div>
);
}
В итоге в родительском компоненте мы по ref-ссылке получим доступ к стейту дочернего компонента. В объект можно передавать любые данные и управлять ими из родительского компонента.
useLayoutEffect
Аналог useEffect
, только работает иначе, он блокирует браузер от перерисовки, пока не отработает полностью. В большинстве случаев в нём нет нужды, так как он блокирует отображение, но также есть ситуации, когда отображение зависит от результата выполненного алгоритма.
К примеру нам нужно узнать, какой будет размер DOM элемента после того, как в нём появится контент, этот размер нам необходим для правильного расчёта и позиционирования другого элемента относительно текущего. Если делать эту операцию с помощью useEffect
, то сначала мы увидим неспозиционированный элемент (курсор), а потом после просчёта он встанет на правильную позицию, в итоге можно наблюдать эффект дёрганья, мерцания.
function sleep(milliseconds) {
const time = Date.now() + milliseconds;
while (Date.now() < time) {
}
}
function Practice(props) {
const { text } = props;
const refText = useRef(null);
const refTip = useRef(null);
const [tipWidth, setTipWidth] = useState(0);
useLayoutEffect(() => {
const { width } = refText.current.getBoundingClientRect();
const widthTip = refTip.current.getBoundingClientRect().width / 2;
sleep(2000);
setTipWidth(width / 2 - widthTip);
}, [text]);
sleep(200);
return (
<div className='practice'>
<p ref={refText}>{text}</p>
<span ref={refTip} style={{ position: 'absolute', left: `${tipWidth}px` }}>||</span>
</div>
);
}
sleep
тут только для наглядного сравнения как будет происходить отображение с useEffect
и useLayoutEffect
. В нашем случае сначала выполнится рендер, jsx элементы запишут ссылку в ref, после выполнится useLayoutEffect
, где мы изменим стейт, благодаря чему рендер будет вызван повторно и мы увидим на экране результат. Наш курсор из тега span
будет находиться посередине текста.useDebugValue
Принимает два аргумента:
- значение, которое вы хотите отобразить в React DevTools. Оно может иметь любой тип
- функция форматирования. Когда компонент проверен, React DevTools вызовет функцию форматирования с аргументом (value), а затем отобразит возвращенное форматированное значение (которое может иметь любой тип)
Для того, чтобы увидеть работу этого хука, нужно установить в ваш браузер расширение React Developer Tools.
useDebugValue
может использоваться для отображения метки (помечаем вкладку статусом) пользовательских хуков в React DevTools.
function useCustomHook() {
const [isOnline, setIsOnline] = useState(false);
useDebugValue(isOnline ? 'В сети' : 'Не в сети', (value) => value.toUpperCase());
return isOnline;
}
В результате мы увидим: CustomHook «НЕ В СЕТИ»
useId
useId
создаёт уникальный идентификатор, связанный с этим конкретным useId
вызовом в этом конкретном компоненте.
useId
не предназначен для генерации ключей в списке. Ключи должны быть сгенерированы из ваших данных.
function Practice() {
const id = useId();
return (
<div className='practice'>
<label>
Пароль:
<input type="password" aria-describedby={id} />
</label>
<p id={id}>Пароль должен быть не короче 8 символов</p>
</div>
);
}
Такие атрибуты доступности HTML , как aria-describedby
, позволяют указать, что два тега связаны друг с другом. Например, вы можете указать, что определенный элемент (например, ввод) описывается другим элементом (например, абзацем).
Мы можем на одной странице создать несколько экземпляров <Practice />
и у них у всех будут уникальные id, что не вызовет конфликта.
id выглядит примерно так: ':r0:'
или ':r1:'
или ':r2:'
и т.д.
useDeferredValue
Принимает значение, которое вы хотите отложить.
Во время первоначального рендеринга возвращаемое отложенное значение будет таким же, как и предоставленное вами значение. Во время обновлений React сначала попытается выполнить повторный рендеринг со старым значением (поэтому возвращаемое значение будет соответствовать старому значению), а затем попытается выполнить еще один повторный рендеринг в фоновом режиме с новым значением (поэтому возвращаемое значение будет соответствовать обновленному значению). Если новые данные поступают быстрее, чем успевает выполняться рендер в фоновом режиме, то он будет прерываться и обрабатывать новые данные, в итоге на экране мы увидим обновление только после окончания поступления новых данных (к примеру, ввод с клавиатуры).
useDeferredValue
позволяет кешировать значения, возвращать старые в отсутствие новых, тем самым откладывает рендер на потом.
Если используем этот хук, то не нужно использовать useTransition
.
Применение:
- Отображение устаревшего контента во время загрузки нового контента
- Указание на то, что содержимое устарело
- Отсрочка повторного рендеринга
function Practice() {
const [text, setText] = useState('');
const deferred = useDeferredValue(text);
const isLoading = useRef(true);
isLoading.current = true;
const list = useMemo(() => {
const dataList = [];
isLoading.current = false;
for (let i = 0; i < 10000; i++) {
dataList.push({
id: i,
text: deferred,
});
}
return dataList;
}, [deferred]);
const handleInputChange = (event) => {
setText(event.target.value);
};
return (
<div className='practice'>
<label> Введите текст:
<input type="text" value={text} onChange={handleInputChange} />
</label>
{
isLoading.current ? <span>🔄 Загрузка...</span> :
<ul>
{list.map((item) => {
return <li key={item.id}>{item.text}</li>;
})}
</ul>
}
</div>
);
}
deferred
могут оказаться старые данные, так как рендер не успевает завершиться с новыми, соответственно useMemo
не будет выполнять функцию со сложным алгоритмом а просто вернёт предыдущий результат, что сильно увеличивает производительность и отзывчивость ввода.useTransition
Возвращает массив с двумя элементами:
isPending
— указывает, когда стоит показать процесс ожидания загрузкиstartTransition
— функция, которая позволяет обернуть другую функцию и пометить её как не особо важную операцию, тем самым будет выполнять её и все последующие потоки параллельно, не заставляя рендер ждать выполнение
function Practice() {
const [text, setText] = useState('');
const [list, setList] = useState([])
const [isPending, startTransition] = useTransition()
const handleInputChange = (event) => {
const { value } = event.target;
setText(value);
startTransition(() => {
const dataList = [];
for (let i = 0; i < 10000; i++) {
dataList.push({
id: i,
text: value,
});
}
setList(dataList);
});
};
return (
<div className='practice'>
<label> Введите текст:
<input type="text" value={text} onChange={handleInputChange} />
</label>
{
isPending ? <span>🔄 Загрузка...</span> :
<ul>
{list.map((item) => {
return <li key={item.id}>{item.text}</li>;
})}
</ul>
}
</div>
);
}
handleInputChange
будет также выполняться и startTransition
вместе с функцией, которая ей передана, но её выполнение никак не будет блокировать рендер и приоритетные процессы, такие как ввод с клавиатуры, поэтому интерфейс будет по-прежнему оставаться отзывчивым. А пока операции в startTransition
не завершены, isPending
будет иметь значение true, что означает процесс выполнения операции.