Debounced Search Component in Vue.js

From Options API to Composition API

Fotis Adamakis
5 min readDec 10, 2022

Search is an essential functionality for most real-life applications and a very common coding interview challenge. Let's implement a simple application that fetches data for an API to demonstrate some basic Vue concepts and best practices. If you are only interested in the composition API example, skip ahead.

User Interface

Let's keep things simple by creating a form using just a search input and a button to submit it.

<template>
<form>
<input type="search" placeholder="Search for products..." />
<button>Search</button>
</form>
</template>

Adding Reactivity

Let's introduce Vue. We need a way to track the value of the input, and a v-model is the perfect use case for that. Additionally, we need to perform the search when the button is clicked. We could use a click handler on the button itself, but by using the submit event on the form instead, we have keyboard support for free. Search will be performed by both clicking the button and form submission by pressing the enter key.

<template>
<form @submit.prevent="performSearch">
<input
type="search"
placeholder="Search for products..."
v-model="searchTerm"
/>
<button>Search</button>
</form>
</template>

<script>
export default {
name: "TheSearch",
data() {
return {
searchTerm: "",
};
},
methods: {
performSearch() {
// TODO
// Perform search here
},
},
};
</script>

Search Functionality

For the data, we will use the dummyJSON service that will return some dummy JSON and the native fetch API. To fetch the data, we need to create a link in the form of https://dummyjson.com/products/search?q=Laptop&limit=5 We do that in the getSearchUrl helper using the native URLSearchParams functionality.

We set the response to a reactive variable called products.

<script>
export default {
name: "TheSearch",
data() {
return {
searchTerm: "",
products: [],
};
},
methods: {
async performSearch() {
const searchUrl = this.getSearchUrl();
const response = await (await fetch(searchUrl)).json();

this.products = response.products;
},
getSearchUrl() {
const url = "https://dummyjson.com/products/search";
const params = {
q: this.searchTerm,
limit: 5,
};

const searchParams = new URLSearchParams(params);
return `${url}?${searchParams}`;
},
},
};
</script>

Debouncing

You might have noticed that we will perform a search every time the form is submitted. This might overflow our server and cause some weird behaviour if some responses fulfil in a different order than they were dispatched. To avoid this, we can introduce debouncing.

Debouncing is a technique that wraps our function with a proxy function which, as long as it continues to be invoked, will not be triggered. The function will be called after it stops being called for a defined amount of milliseconds.

We could implement our own debounce function, but we will use an npm package instead.

The syntax feels a bit unnatural but should be more clear after seeing the debounce function signature debounce(fn, wait, [ immediate || false ])

<script>
import { debounce } from "debounce";

export default {
...
methods: {
performSearch: debounce(
async function () {
const searchUrl = this.getSearchUrl();
const response = await (await fetch(searchUrl)).json();

this.products = response.products;
},
600
),
...
},
};
</script>

Search will be performed a maximum of once every 600, no matter how many times it is invoked.

Search Automatically

Having a button to search is not common nowadays. Let's perform the search automatically using a watcher instead.

Additionally, we added some validations in case the search term is empty or too short.

<script>
...
watch: {
searchTerm() {
this.performSearch();
},
},
methods: {
performSearch: debounce(async function () {
if (this.searchTerm === "") {
this.products = [];
return;
}
if (this.searchTerm.length < 2) {
return;
}
const searchUrl = this.getSearchUrl();
const response = await (await fetch(searchUrl)).json();

this.products = response.products;
}, 600),
...
</script>

Search Results

Nothing exciting about this component, but we can’t have a search functionality without results.

<template>
<section>
<h2>Products</h2>
<div v-for="product in products" :key="product.id">
<img :src="product.thumbnail" alt="product.title" />
<h5>
{{ product.title }}
</h5>
<p>
{{ product.description }}
</p>
<hr />
</div>
</section>
</template>

<script>
export default {
name: "SearchResults",
props: ["products"],
};
</script>

Used like this

<template>
...
<SearchResults v-if="products.length" :products="products" />
</template>

<script>
import SearchResults from "./SearchResults.vue";

export default {
name: "TheSearch",
components: {
SearchResults,
},
...
};
</script>

Final Solution with Options API

You can find the final solution for a debounced search component using options API in the following sandbox.

Composition API

Vue 3 is here, and using composition API will soon be the norm. Converting our solutions to use it will be interesting. Let's do that.

The Search Results component conversion is trivial since it's only a presentation component.

Everything remains the same in the template, and we have to use the defineProps helper inside script setup to make the template aware of the products.

<template>
<section>
<h2>Products</h2>
<div v-for="product in products" :key="product.id">
<img :src="product.thumbnail" alt="product.title" />
<h5>
{{ product.title }}
</h5>
<p>
{{ product.description }}
</p>
<hr />
</div>
</section>
</template>

<script setup>
defineProps(["products"]);
</script>

The search component is a bit more complicated. The steps we need to do to convert it are the following:

  • Add setup to the script tag
  • Declare data using ref
  • Update the references to data by adding .value
  • Remove this everything can be accessed directly
  • Declare watcher using the watch helper

Remember everything declared inside the script setup will be automatically available in the component template. This includes variables, functions and imported components.

<template>
// Remains the same
</template>

<script setup>
import { debounce } from "debounce";
import { ref, watch } from "vue";
import SearchResults from "./SearchResults.vue";

const searchTerm = ref("");
const products = ref([]);

const getSearchUrl = () => {
const url = "https://dummyjson.com/products/search";
const params = {
q: searchTerm.value,
limit: 5,
};
const searchParams = new URLSearchParams(params);
return `${url}?${searchParams}`;
};

const performSearch = debounce(async () => {
if (searchTerm.value === "") {
products.value = [];
return;
}
if (searchTerm.value.length < 2) {
return;
}
const searchUrl = getSearchUrl();
const response = await (await fetch(searchUrl)).json();

products.value = response.products;
}, 600);

watch(searchTerm, () => {
performSearch();
});
</script>

Final Solution with Composition API

You can find the final solution for a debounced search component using composition API in the following sandbox.

Conclusion

So here you go. A simple search component using debounce in both Options and Composition API formats. The new format takes some time to get used to, but it's definitely cleaner, and I believe soon it will be the way we write our components.

This article was inspired by the Advert Of Vue 2022 Day #1 Challenge.

--

--

« Senior Software Engineer · Author · International Speaker · Vue.js Athens Meetup Organizer »