Сегодня я собираюсь показать вам, как создать хук useDebounce, который позволяет супер-просто отложить вызовы АПИ, что бы они не происходили слишком часто.
Так же я создал демо, которое использует наш хук. Оно ищет по АПИ Marvel Comics и хук useDebounce позволяет избежать обращений к серверу на каждое нажатие клавиши.
Довольно изящно, да? Окей, теперь перейдем к коду!
Сначала давайте разберемся, как бы мы хотели, чтобы наш хук использовали и тогда мы сможем руководствоваться этим при непосредственной реализации логики самого хука. Вместо того, чтобы откладывать вызов только нашего АПИ запроса, мы собираемся спроектировать этот хук таким образом, чтобы он мог отложить вычисление любого значения внутри функции рендера нашего компонента. Мы собираемся совместить это с хуком useEffect, который будет обращаться к АПИ каждый раз, когда наше отложенное значение изменится. Пример кода ниже подразумевает некоторое знакомство с хуками useState и useEffect, о которых вы можете прочитать подробнее в документации по хукам Реакта.
import React, { useState, useEffect } from 'react';
import useDebounce from './use-debounce';
// Использование
function App() {
// Состояние и сеттер состояния для поискового запроса
const [searchTerm, setSearchTerm] = useState('');
// Состояние и сеттер состояния для результатов поиска
const [results, setResults] = useState([]);
// Состояние для статуса поиска (есть ли ожидающий запрос API)
const [isSearching, setIsSearching] = useState(false);
// Теперь мы вызываем наш хук, передавая текущее значение searchTerm.
// Хук вернет только последне значение (которое мы передали) ...
// ... если прошло более 500ms с последнего вызова.
// Иначе он вернет предыдущее значение searchTerm.
// Цель в том, чтобы вызвать АПИ только после того, как пользователь перестанет
// печатать и в итоге мы не будем вызвать АПИ слишком часто.
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// Здесь происходит вызов АПИ
// Мы используем useEffect, так как это асинхронное действие
useEffect(
() => {
// Убедиться что у нас есть значение (пользователь ввел что-то)
if (debouncedSearchTerm) {
// Выставить состояние isSearching
setIsSearching(true);
// Сделать запрос к АПИ
searchCharacters(debouncedSearchTerm).then(results => {
// Выставить состояние в false, так-как запрос завершен
setIsSearching(false);
// Выставит состояние с результатом
setResults(results);
});
} else {
setResults([]);
}
},
// Это массив зависимостей useEffect
// Хук useEffect сработает только если отложенное значение изменится ...
// ... и спасибо нашему хуку, что оно изменится только тогда ...
// когда значение searchTerm не менялось на протяжении 500ms.
[debouncedSearchTerm]
);
// Довольно стандартный UI с полем поиска и результатами
return (
<div>
<input
placeholder="Search Marvel Comics"
onChange={e => setSearchTerm(e.target.value)}
/>
{isSearching && <div>Searching ...</div>}
{results.map(result => (
<div key={result.id}>
<h4>{result.title}</h4>
<img
src={`${result.thumbnail.path}/portrait_incredible.${
result.thumbnail.extension
}`}
/>
</div>
))}
</div>
);
}
// Функция поиска по АПИ
function searchCharacters(search) {
const apiKey = 'f9dfb1e8d466d36c27850bedd2047687';
const queryString `apikey=${apiKey}&titleStartsWith=${search}`;
return fetch(
`https://gateway.marvel.com/v1/public/comics?${queryString}`,
{
method: 'GET'
}
)
.then(r => r.json())
.then(r => r.data.results)
.catch(error => {
console.error(error);
return [];
});
}
Окей, выглядит довольно хорошо! Давайте теперь создадим непосредственно сам хук, чтобы наше приложение работало.
import React, { useState, useEffect } from 'react';
// Наш хук
export default function useDebounce(value, delay) {
// Состояние и сеттер для отложенного значения
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Выставить debouncedValue равным value (переданное значение)
// после заданной задержки
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Вернуть функцию очистки, которая будет вызываться каждый раз, когда ...
// ... useEffect вызван снова. useEffect будет вызван снова, только если ...
// ... value будет изменено (смотри ниже массив зависимостей).
// Так мы избегаем изменений debouncedValue, если значение value ...
// ... поменялось в рамках интервала задержки.
// Таймаут очищается и стартует снова.
// Что бы сложить это воедино: если пользователь печатает что-то внутри ...
// ... нашего приложения в поле поиска, мы не хотим, чтобы debouncedValue...
// ... не менялось до тех пор, пока он не прекратит печатать дольше, чем 500ms.
return () => {
clearTimeout(handler);
};
},
// Вызывается снова, только если значение изменится
// мы так же можем добавить переменную "delay" в массива зависимостей ...
// ... если вы собираетесь менять ее динамически.
[value]
);
return debouncedValue;
}
И вот он перед вами! У нас теперь есть хук для отложенных значений с помощью которого мы можем задержать изменение любого значения прямо в теле нашего компонента. Отложенные значение затем могут быть добавлены в массив зависимостей useEffect, вместо самих изменяемых значений, что бы ограничить частоту вызовов нужного эффекта.
Вот демо Marvel Comic Search demo на CodeSandbox.
Если вам понравилось, то можете заглянуть и оценить мой блог о хуках Реакта(на англ) и React app builder.
justboris
Стоит заметить, что в будущих версиях реакта будет доступен встроенный хук – useDeferredValue, который заменит этот кастомный useDebounce
CodeShaman Автор
Да, будет замечательно, когда он появится. Хорошее замечание.
Пока этот показался очень удобным решением в несколько строчек, если нет желания и необходимости тащить в проект дополнительные зависимости для denouncing’га.