Блог

2025 / piscodev / Игорь Быстрицкий

обложка статьи
Блог

Debounce. Оптимизация React приложения

Debounce — это разовый вызов функции после "этапа успокоения". Узнайте, как этот метод позволяет оптимизировать приложение.

    • ReactTypeScript
  • Нормально

  • 24 декабря 2023
  • Игорь Быстрицкий

Debounce это что?Добавление debounce в компонентХук useDebounceCallbackParamCancelЗаключение
Если понравилась статья, поделитесь с друзьями

TypeScript / Больше статей

Cookie в Next

Cookie в Next

Как создавать и читать Cookie. Функции-обертки на TypeScript с удобной типизацией.

01 февраля 2024
Типизация страницы в Next

Типизация страницы в Next

Используй TypeScript и Next вместе, чтобы создать кастомный тип TNextPage, с помощью которого можно с легкостью указать типы params и searchParams на любой странице.

20 июня 2024

Debounce это что?

Debounce переводится как подавление или устранение, что относится к излишнему, ненужному вызову функций. Это метод оптимизации, согласно которому состояние не обновится, пока после последней попытки изменить состояние не пройдет некоторое время. Цель debounce — сократить количество ререндеров.

Debounce необходим в разработке приложений с каким-либо состоянием. Это относится к input и textarea, в которые юзер непрерывно вводит новые символы, тем самым перезаписывая данные в состоянии, что вызывает ререндер компонента каждый раз:

Пример

TypeScript JSX

import { useState }, React from 'react'

function ReactComponent() {
	const [name, setName] = useState('')

	function onChange(e: React.ChangeEvent<HTMLInputElement>) {
		setName(e.target.value)
	}

	return (
		<article>
			<input type='text' onChange={onChange} />
		</article>
	)
}
Набирая текст в инпут, юзер будет вызывать ререндер компонента каждый раз!

Вы должны понимать, что лишние ререндеры — это причина лагов в приложении. Когда тогда нужно вызывать updateName? Все просто:

  • после ввода символа в инпут должна пройти 1 секунда, прежде чем обновится состояние;
  • если в течение этой секунды пользовать продолжил вводить что-либо в инпут, то отсчет 1 секунды начинается снова,
  • когда проходит больше 1 секунды, состояние наконец обновляется.

Этот подход и называется debounce. Остается лишь надеяться, что пользователь пишет достаточно быстро, а не по одному символу в секунду, чтобы этот метод работал.

Добавление debounce в компонент

Debounce использует таймер ⌚, который нам придется использовать вместе с useRef, чтобы он случайно не ререндерился (и не сбрасывался) вместе с родителем, ведь ему нужно работать независимо от жизненного цикла родителя:

TypeScript JSX

import { useState }, React from 'react'

function ReactComponent() {
	const [name, setName] = useState('')
	const timer = useRef<NodeJS.Timeout | null>(null)

	function onChange(e: React.ChangeEvent<HTMLInputElement>) {
		timer.current && clearTimeout(timer.current)
		timer.current = setTimeout(() => {
			props.callback(param)
			setName(e.target.value)
		}, 1000)
	}

	return (
		<article>
			<input type='text' onChange={onChange} />
		</article>
	)
}

Теперь функция updateName будет сначала сбрасывать активный таймер, а затем запускать его снова со значением в 1 секунду. Вместе с тем как юзер печатает текст в input, состояние name не обновится, никаких ререндеров происходить не будет, пока он не перестанет печатать.

Хук useDebounce

Если у вас есть трудности в написанни кастомных хуков, рекомендую сначала ознакомиться с ними в этой статье.

Удобно было бы использовать этот функционал по всему приложению. Напишем хук. Придется повозиться с типами, а так ничего сложного в нем не ждите.

TypeScript JSX

import { useRef } from 'react'

type TUseDebounce<CallbackParam> = {
	seconds: number
	callback: (param: CallbackParam) => void
}

export function useDebounce<CallbackParam>(props: TUseDebounce<CallbackParam>) {
	const timer = useRef<NodeJS.Timeout | null>(null)

	function call(param: CallbackParam, onComplete?: (param: CallbackParam, seconds: number) => void) {
		cancel()
		timer.current = setTimeout(() => {
			props.callback(param)
			onComplete && onComplete(param, props.seconds)
		}, props.seconds * 1000)
	}

	function cancel() {
		timer.current && clearTimeout(timer.current)
	}

	return [call, cancel] as const
}
Важно указать as const, чтобы обозначить tuple (еще называют кортеж) и TypeScript не смешивал типы этих двух фукнций, думая, что мы возвращаем бесформенный массив. 🧠

useDebounce можно использовать следующим образом:

Пример

TypeScript

const [onNameChanged] = useDebounce({
	callback: (value: string) => {
		// Какой-то код, изменяющий состояние. ✍
	},
	seconds: 1,
})
onNameChanged — это функция call, которая запустит callback после таймера.

В компоненте:

Пример

TypeScript JSX

import { useState }, React from 'react'
import { useDebounce } from '@/utils'

function ReactComponent() {
	const [name, setName] = useState('')
	const [onNameChanged, cancelNameChange] = useDebounce({
		callback: (value: string) => {
			setName(value)
		},
		seconds: 1,
	})

	function delayOnChange(e: React.ChangeEvent<HTMLInputElement>) {
		onNameChanged(e.target.value)
	}

	return (
		<article>
			<input type='text' onChange={delayOnChange} />
			<button onClick={cancelNameChange}>Отменить обновление имени!!!</button>
		</article>
	)
}

CallbackParam

CallbackParam это дженерик функции, которую мы скормим хуку, вызывая его. Этот тип нужен функции call, которую нам хук предоставляет для вызова этой самой функции.

Cancel

Также хук вернет функцию cancel, которая нам может вдруг понадобиться для отмены вызова фукнции вообще. Пока что у меня не было такой необходимости, но на всякий случай пусть будет 😉.

Время пройти тест

Проверьте свои знания по текущей теме

Что делает as const после массива?

Заключение

Используйте debounce, если вам нужно оптимизировать сложный React компонент с тяжелым жизненным циклом.