Выноси логику с состоянием в переиспользуемые, компонуемые и тестируемые хуки.
Открыть этот урок в KodokonСвой хук - это функция, имя которой начинается с use и которая вызывает другие хуки. Он выносит логику с состоянием, а не общее состояние: каждый компонент, который его вызывает, получает свои собственные useState и useEffect, полностью изолированные. Это самый прямой инструмент React для вынесения логики - там, где HOC и render props добавляли слои в дерево, хук просто встраивается в тело компонента, легко компонуется и легко тестируется.
import { useEffect, useState } from "react";
function useDebouncedValue(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => {
setDebounced(value);
}, delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}Префикс use не для красоты: именно он позволяет ESLint-плагину react-hooks проверять правила хуков внутри твоей функции - вызовы без условий, на верхнем уровне. Позаботься и о форме возвращаемого API: кортеж [value, setValue], когда использование повторяет useState, и объект с именованными ключами, как только наружу нужно отдать больше двух вещей.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored !== null
? JSON.parse(stored)
: initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}Хуки компонуются: хук может вызывать другие хуки, в том числе твои собственные. Так и появляются читаемые бизнес-абстракции, построенные на универсальных кирпичиках. Обрати внимание на связку AbortController с функцией очистки: без неё запрос, отправленный по более старому вводу, может ответить после самого свежего и перезаписать его результат - классическая гонка в полях поиска.
function useUserSearch(query) {
const debounced = useDebouncedValue(query, 400);
const [results, setResults] = useState([]);
useEffect(() => {
if (debounced === "") {
setResults([]);
return;
}
const controller = new AbortController();
fetch("/api/search?q=" + debounced, {
signal: controller.signal,
})
.then((res) => res.json())
.then(setResults)
.catch(() => {});
return () => controller.abort();
}, [debounced]);
return results;
}