How to debounce in Svelte?

Justin Ahinon Updated

Resources

Svelte version: 4.0.5

Here is a scenario (inspired from a real use case I had):

  • You are trying to build a search interface for Subreddits on Reddit

  • Every time the user types a character, you want to fetch the list of Subreddits that match the query

  • And display the results as a list

But here are some problems:

  • If you send a request to Reddit every time the user types a character, you will quickly hit the API rate limit

  • Depending on which response you get first, you might end up displaying the results of a query that is not the latest one

How can you solve this problem?

That's the purpose of this article. We will see how to use the debounce technique to solve this problem.

But first, what does "debouncing" mean?

Debouncing is a technique that allows you to delay the execution of a function until a certain amount of time has passed since the last time the function was called.

Think of it this way: you have a button that you can click. Every time you click the button, you want to display a message. But you don't want to display the message every time you click the button. You want to display the message only if you haven't clicked the button for 2 seconds.

Debouncing is a technique that allows you to do that.

Generally, to debounce a function:

  • You need to keep track of the last time it was invoked

  • When the function is called, you check how much time has elapsed since the last invocation

  • If it's been less than the delay you want (e.g. 500ms), you do nothing

  • If the delay has elapsed, you invoke the function and reset the timer

The benefit is it limits how often the function is actually run. So you can avoid issues like hitting API limits or performing expensive operations too frequently.

Now, let's see how we can apply this technique to our Subreddits search interface.

Naive search interface implementation

Here is how I'd naively and intuitively implement the search interface:

<script lang="ts">
	import type { RedditAutoCompleteResponse } from './autocomplete';

	let foundSubreddits: string[] = [];

	const searchSubreddit = async (event: KeyboardEvent) => {
		const input = event.target as HTMLInputElement;
		const query = input.value;

		if (!query || query.length === 0) {
			foundSubreddits = [];
			return;
		}

		const response = (await (
			await fetch(
				`https://www.reddit.com/api/subreddit_autocomplete_v2.json?query=${query}&limit=10`
			)
		).json()) as RedditAutoCompleteResponse;

		foundSubreddits = response.data.children.map((child) => {
			return child.data.display_name;
		});
	};

	$: console.log(foundSubreddits);
</script>

<svelte:head>
	<title>Blank</title>
</svelte:head>

<main>
	<form>
		<input name="search" type="search" placeholder="Search subreddits" on:keyup={searchSubreddit} />

		{#if foundSubreddits.length > 0}
			<ul>
				{#each foundSubreddits as subreddit}
					<li class="flex px-4 py-2 border border-gray-200 bg-gray-50 hover:bg-gray-200">
						r/{subreddit}
					</li>
				{/each}
			</ul>
		{/if}
	</form>
</main>

I believe the code above is pretty straightforward.

  • We have a searchSubreddit function that is called every time the user types a character on the input

  • The function fetches the list of Subreddits that match the query and stores them in the foundSubreddits reactive variable

  • The foundSubreddits variable is used to display the list of Subreddits

Here is what it looks like:

You will notice in the screen recording that the list of Subreddits is updated every time I type a character, and the experience is far from ideal. Plus, in that screen recording, I was typing pretty slowly. Imagine if I was typing faster.

Debouncing the search function

Instead of directly calling the searchSubreddit function every time the user types a character, we will rather call a custom debounce function that will take the searchSubreddit function as a callback argument.

Here is how the debounce function looks like:

const debounce = (callback: Function, wait = 300) => {
    let timeout: ReturnType<typeof setTimeout>;

    return (...args: any[]) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => callback(...args), wait);
    };
};

Here is what's happening:

  • We keep track of the timeout variable

  • Every time the function is called, we clear the timeout

  • We set a new timeout that will call the callback function after the delay we want (300ms by default)

  • If the function is called again before the delay has elapsed, we clear the timeout and set a new one

Now, not many things change from our previous implementation. We just need to call the debounce function instead of the searchSubreddit function:

<input name="search" type="search" placeholder="Search subreddits" on:keyup={debounce(autocompleteSubreddits)} />

Now, it's not the search function that is called directly. Our debounce function will be called every time the user types a character. And the debounce function will call the search function only if the delay has elapsed.

Here is what it looks like now:

Much better, right? The experience is way smoother. And we can properly control how often the search function is called with the delay we want.

Conclusion

That's it for this article. I hope you enjoyed it and learned something new.