Используем Throttle и Debounce в React
Tags: react, debounce, throttle, javascript
Published: 2022-05-28 12:00:34

## Обзор
`Throttle` и `Debounce` решают задачи оптимизации.

`Throttle` - пропускает вызовы функции с определённой периодичностью.
`Debounce` - откладывает вызов функции до того момента, когда с последнего вызова пройдёт определённое количество времени.

*Throttle & Debounce схема:*

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7q3kw8dwjsbcv4dux07.png)

#### Примеры использования Throttle:
1) Если пользователь изменяет размер окна браузера и нам необходимо изменять содержимое сайта.
Без оптимизации происходит следующее. При каждом событии изменения размера окна вызывается обработчик события изменения размера окна. Таким образом, если пользователь, например, изменяет размер окна в течение 10 секунд, то может произойти 100, 200 и т.д. событий, которые нам нужно обработать.
`Throttle` позволяет нам задать временной интервал, чаще которого обработчик события вызываться не будет. Если мы, используя `Throttle`, укажем интервал в 1 секунду, то кол-во выполнения обработчиков события изменения размера окна будет равно 10.

2) Показ пользователю количества процентов прокрутки страницы. При прокрутке страницы пользователем возникают события `scroll`, которые нам необходимо обработать. С помощью `throttle` мы можем уменьшать кол-во обрабатываемых событий прокрутки пользователем страницы, задав временной интервал.

#### Примеры использования Debounce:
1) Обработка данных поискового запроса пользователя.
При введении пользователем поискового запроса, ему  предлагаются варианты поиска. Это происходит следующим образом. 
При изменении вводимого пользователем текста, на сервер передаётся запрос, в котором мы передаем уже напечатанные символы. Затем получаем ответ от сервера с возможными вариантами поискового запроса и показываем их пользователю.
При каждом изменении текста пользователем, вызывается обработчик события, в котором делается запрос на сервер.
Для оптимизации количества отправляемых запросов на сервер используем `Debounce`.
При изменении текста пользователем, применение `Debounce` позволяет нам создать таймер, например на 1 секунду. Если 1 секунда проходит, и пользователь не изменил текст второй раз, то вызывается обработчик события и выполняется запрос к серверу. Если пользователь изменяет текст второй раз за 1 секунду, то 1-й таймер сбрасывается и создаётся новый таймер опять на 1 секунду.
Таким образом, если пользователь редактирует поисковый текст быстро (быстрее 1 секунды), то запрос отправится на сервер только один раз, после того, как пользователь прекратит печатать.

2) Отправка данных аналитики на сервер. Например, пользователь водит мышкой по сайту, мы записываем координаты мышки в массив, после чего `Debounce` позволяет отправить информацию о перемещении мышки клиента на сервер только после того, как клиент перестаёт двигать мышкой.

Итак, в этой статье я покажу, как использовать `Throttle` и `Debounce` в React приложении.

## Шаг 1 - Шаблон приложения
Создадим шаблон приложения с помощью `create-react-app` и запустим его:
```console
npx create-react-app throttle-debounce
cd throttle-debounce
npm start
```
Заменяем содержимое файла `App.css` своими стилями:
```css
body {
    display: flex;
    justify-content: center;
    width: 100%;
}
h1 {
    text-align: center;
    margin: 0.5rem 0;
}
.l-scroll {
    overflow-y: scroll;
    overflow-x: hidden;
    width: 380px;
    height: 200px;
    margin-top: 0.5rem;
}
.scroll-content {
    width: 100%;
    background-color: bisque;
    padding: 0 1rem;
}
.l-scroll::-webkit-scrollbar {
    width: 10px;
    height: 8px;
    background-color: darkturquoise;
}
.l-scroll::-webkit-scrollbar-thumb {
    background-color: blueviolet;
}
```
Заменим содержимое файла `App.js` на шаблон нашего приложения:
```jsx
import './App.css';
import { useMemo } from 'react';

function App() {
    return (
        <>
            <h1>Throttle & Debounce</h1>
            <div className="l-scroll">
                <div className="scroll-content">
                    <TallContent />
                </div>
            </div>
        </>
    );
}

// Прокручиваемый контент большой высоты
function TallContent(){
    const dataElements = useMemo(() => {
        const genData = [];
        for(let i=1; i<=200; i++){
            genData.push(
                <div key={i}>Line: {i}</div>
            );
        }
        return genData;
    }, []);
    
    return(
        <>
            {dataElements}
        </>
    );
}

export default App;
```
Шаблон приложения готов, приступим ко второму шагу - обычный обработчик событий прокрутки.

## Шаг 2 - Обычный обработчик событий
Здесь мы добавим обычный обработчик для `scroll` событий и посчитаем количество вызовов этого обработчика при прокрутке пользователем элемента страницы.

Добавим состояние количества вывозов обработчика событий в `App` компонент:
```jsx
// В начале файла
import { useState, useMemo } from 'react';
// Внутри компонента App
const [scrollHandleCount, setScrollHandleCount] = useState(0);
```
Затем добавим обработчик события прокрутки, для этого добавим аттрибут `onScroll` на элемент под `h1` заголовком:
```jsx
// Было
<div className="l-scroll">
    ...
</div>

// Стало
<div className="l-scroll" onScroll={handleScroll}>
    ...
</div>
```
Также добавим функцию обработки события `handleScroll` в компонент `App`:
```jsx
function handleScroll(){
    handleUsualScroll();
}
```
Внутри функции `handleScroll` мы поместили функцию в которой будет происходить обработка обычного события. Добавим эту функцию в наш `App` компонент:
```jsx
function handleUsualScroll(){
    setScrollHandleCount((prevState) => {
        return ++prevState;
    });
}
```
Осталось только показать состояние счётчика пользователю, для этого добавим строку кода под `h1` заголовком:
```jsx
<span>
   Usual scroll handle count: {scrollHandleCount}
</span>
<br />
```
Теперь, при прокрутке элемента на странице, мы должны увидеть кол-во вызовов функции `handleUsualScroll()`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uzl1ynovnmke8uk8bc85.gif)

Полный код компонента `App` на данный момент:
```jsx
function App() {
    const [scrollHandleCount, setScrollHandleCount] = useState(0);
    return (
        <>
            <h1>Throttle & Debounce</h1>
            <span>
                Usual scroll handle count: {scrollHandleCount}
            </span>
            <br />
            <div className="l-scroll" onScroll={handleScroll}>
                <div className="scroll-content">
                    <TallContent />
                </div>
            </div>
        </>
    );
    
    function handleScroll(){
        handleUsualScroll();
    }
    function handleUsualScroll(){
        setScrollHandleCount((prevState) => {
            return ++prevState;
        });
    }    
}
```

## Шаг 3 - Обработчик событий с Throttle
`Throttle` обработчик событий в нашем случае должен вызывать увеличение счётчика `scrollThrottleHandleCount`, при этом ***пропускать вызовы увеличения счётчика с определённой периодичностью***.
Для реализации задуманного нам потребуется таймер при запуске которого состояние `Throlle` переходит в `In progress`. При этом если состояние `In Progerss`, то обработка событий пользователя (прокрутка элемента страницы) пропускается.
Как только таймер срабатывает, состояние `Throttle` переходив в `Not in progress`, а значит наш обработчик будет снова обрабатывать события пользователя. Таким образом происходит пропуск событий пользователя с заданным интервалом времени.
Реализуем вышеописанное:
```jsx
// Добавим useRef для хранения состояния inProgress
import { useState, useRef, useMemo } from 'react';
```
Далее в компоненте `App` добавим состояние счётчика вызовов обработчика событий с `Throttle` и `ref` для хранения состояния `inProgress`:
```jsx
// Кол-во вызовов обработчика событий с Throttle
const [
   scrollThrottleHandleCount,
   setScrollThrottleHandleCount
] = useState(0);
// Храним состояние in progress
const throttleInProgress = useRef();
```
Здесь важно отменить, что `throttleInProgress` - часть сайд эффекта связанного с таймером, а значит, состояние мы будем хранить в `ref` объекте, так как `useRef` возвращает объект существующий на протяжении всего жизненного цикла компонента, при этом не происходит лишнего рендеринга компонента при изменении свойства `current` объекта возвращаемого `useRef`, в отличии от `useState`.
Теперь добавим сам обработчик события с `Throttle` в `App` component:
```jsx
function handleThrottleScroll(){
    // Если состояние inProgress - выходим из функции,
    // пропускаем обработку события 
    if(throttleInProgress.current){ return; }
    // Устанавливаем inProgress в true и запускаем таймер
    throttleInProgress.current = true;
    setTimeout(() => {
        // Увеличиваем состояние throttleHandleCount
        // на единицу
        setScrollThrottleHandleCount((prevState) => {
            return ++prevState;
        });
        // inProgress устанавливаем в false,
        // значит при следующем запуске
        // setTimeout снова сработает
        throttleInProgress.current = false;
    }, 500);
}
```
Осталось 2 простых действия: добавить отображение состояния счётчика с `Throttle` пользователю и добавить `handleThrottleScroll()` в `handleScroll()`:
```jsx
// После заголовка h1
<span>
   Throttle scroll handle count: {scrollThrottleHandleCount}
</span>

// В функцию handleScroll() после handleUsualScroll();
handleThrottleScroll();
```
В результате мы получим:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/le10pgilz162y86gal38.gif)
Обычный обработчик событий вызвал бизнес логику приложения 181 раз, а с `Throttle` всего 9.
Полный код компонента `App` с `Throttle`:
```jsx
function App() {
    const [scrollHandleCount, setScrollHandleCount] = useState(0);
    const [
        scrollThrottleHandleCount,
        setScrollThrottleHandleCount
    ] = useState(0);
    const throttleInProgress = useRef();
    
    return (
        <>
            <h1>Throttle & Debounce</h1>
            <span>
                Usual scroll handle count: {scrollHandleCount}
            </span>
            <br />
            <span>
                Throttle scroll handle count: {scrollThrottleHandleCount}
            </span>
            <br />
            <div className="l-scroll" onScroll={handleScroll}>
                <div className="scroll-content">
                    <TallContent />
                </div>
            </div>
        </>
    );
    
    function handleScroll(){
        handleUsualScroll();
        handleThrottleScroll();
    }
    function handleUsualScroll(){
        setScrollHandleCount((prevState) => {
            return ++prevState;
        });
    }
    function handleThrottleScroll(){
        if(throttleInProgress.current){ return; }
        throttleInProgress.current = true;
        setTimeout(() => {
            setScrollThrottleHandleCount((prevState) => {
                return ++prevState;
            });
            throttleInProgress.current = false;
        }, 500);
    }
}
```
Перейдём к заключительному шагу - реализуем `Debounce` обработчик событий.

## Шаг 4 - Обработчик событий с Debounce
`Debounce` в нашем примере откладывает увеличение счётчика `scrollDebounceHandleCount` до того момента, когда ***с последнего вызова*** обработчика события ***пройдёт определённое количество времени***.
Добавим состояние количества вызовов обработчика событий с `Debounce`, `ref` для хранения идентификатора таймера в `App` компонент:
```jsx
const [
    scrollDebounceHandleCount,
    setScrollDebounceHandleCount
] = useState(0);
const timerDebounceRef = useRef();
```
Затем покажем количество `scrollDebounceHandleCount` пользователю и добавим наш метод `handleDebounceScroll()` в `handleScroll()`:
```jsx
// После h1
<span>
    Debound scroll handle count: {scrollDebounceHandleCount}
</span>
// В функцию handleScroll()
handleDebounceScroll();
```
Осталось написать саму функцию `handleDebounceScroll`:
```jsx
function handleDebounceScroll(){
    // Если ID таймена установлено - сбрасываем таймер
    if(timerDebounceRef.current){
        clearTimeout(timerDebounceRef.current);
    }
    // Запускаем таймер, возвращаемое ID таймера
    // записываем в timerDebounceRef
    timerDebounceRef.current = setTimeout(() => {
        // Вызываем увеличение счётчика кол-ва
        // выполнения бизнес логики приложения с Debounce
        setScrollDebounceHandleCount((prevState) => {
            return ++prevState;
        });
    }, 500);
}
```
В результате увеличение счётчика с `Debounce` происходит только тогда, когда пользователь перестаёт прокручивать элемент страницы больше или равным 500 миллисекунд:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n94rs556vj0hzhcascux.gif)
Полный текст `App` компонента:
```jsx
function App() {
    const [scrollHandleCount, setScrollHandleCount] = useState(0);
    const [
        scrollThrottleHandleCount,
        setScrollThrottleHandleCount
    ] = useState(0);
    const [
        scrollDebounceHandleCount,
        setScrollDebounceHandleCount
    ] = useState(0);

    const throttleInProgress = useRef();
    const timerDebounceRef = useRef();
    
    return (
        <>
            <h1>Throttle & Debounce</h1>
            <span>
                Usual scroll handle count: {scrollHandleCount}
            </span>
            <br />
            <span>
                Throttle scroll handle count: {scrollThrottleHandleCount}
            </span>
            <br />
            <span>
                Debound scroll handle count: {scrollDebounceHandleCount}
            </span>
            <div className="l-scroll" onScroll={handleScroll}>
                <div className="scroll-content">
                    <TallContent />
                </div>
            </div>
        </>
    );
    
    function handleScroll(){
        handleUsualScroll();
        handleThrottleScroll();
        handleDebounceScroll();
    }
    function handleUsualScroll(){
        setScrollHandleCount((prevState) => {
            return ++prevState;
        });
    }
    function handleThrottleScroll(){
        if(throttleInProgress.current){ return; }
        throttleInProgress.current = true;
        setTimeout(() => {
            setScrollThrottleHandleCount((prevState) => {
                return ++prevState;
            });
            throttleInProgress.current = false;
        }, 500);
    }
    function handleDebounceScroll(){
        if(timerDebounceRef.current){
            clearTimeout(timerDebounceRef.current);
        }
        timerDebounceRef.current = setTimeout(() => {
            setScrollDebounceHandleCount((prevState) => {
                return ++prevState;
            });
        }, 500);
    }
}
```
Подписывайтесь на блог, ставьте лайки, добавляйте в закладки.
Не забываем про единорогов.

Спасибо за внимание.