watch and Search Functionality

In this chapter, you'll learn to use watch and watchEffect to listen for data changes and implement real-time filtering in the blog search box.


What scenarios need watch?

In most cases, Vue's reactive binding is sufficient: when data changes, the template updates automatically.

But some scenarios require you toproactively execute a piece of logic when data changes, such as:

  • After typing in the search box, filter the article list
  • When the user switches categories, update the page title
  • After data changes, save to localStorage
  • When form fields change, perform form validation

This is when you needwatchandwatchEffect。


watch — Precise Listening

watchListen to specified data sources and execute a callback function when data changes.

You can get thenew valueandold value, for comparison handling.

Example

<script setup>
import { ref, watch } from 'vue'

const keyword = ref('')

// watch(the data source being listened to, callback function)
watch(keyword, (newVal, oldVal) => {
console.log(`The search keyword changed from "${oldVal}" to "${newVal}"`)
// Execute the filtering logic here
})
</script>

<template>
  <input v-model="keyword" placeholder="Search articles...">
  <p>Current search: {{ keyword }}</p>
</template>

Three Data Sources for watch

Data source typeSyntaxExample
Single refwatch(ref, callback)watch(keyword, fn)
Property of a reactive objectwatch(() => obj.prop, callback)watch(() => user.name, fn)
Multiple data sourceswatch([ref1, ref2], callback)watch([kw, cat], fn)

Example

import { ref, watch } from 'vue'

const keyword = ref('')
const category = ref('All')

// Listen to a single ref
watch(keyword, (newVal) => {
  document.title = newVal ?`Search: ${newVal}` : 'EXAMPLE Blog'
})

// Listen to multiple data sources
watch([keyword, category], ([newKw, newCat], [oldKw, oldCat]) => {
  console.log('Keyword or category changed, re-filtering articles')
  // Execute the filtering logic...
})

By default, watch islazy-executing—it won't trigger when data first has a value, only on subsequent changes. If you need it to execute once during initialization, add the{ immediate: true }configuration option.


watchEffect — Automatically Tracks Dependencies

watchEffectNo need to manually specify data sources; it automatically tracks the reactive data used in the callback.

The biggest difference from watch:watchEffect executes immediately once。

Example

<script setup>
import { ref, watchEffect } from 'vue'

const keyword = ref('')
const articles = ref([
{ id: 1, title: 'Vue3 Introduction' },
{ id: 2, title: 'JS Async' },
])

const filteredArticles = ref([])

// watchEffect automatically tracks keyword and articles
// And it executes once immediately to initialize filteredArticles
watchEffect(() => {
// All reactive data used in the function is automatically tracked
  filteredArticles.value = articles.value.filter(
    a => a.title.includes(keyword.value)
  )
console.log(`Filter result: ${filteredArticles.value.length} articles`)
})
</script>

<template>
  <input v-model="keyword" placeholder="Search">
  <div v-for="a in filteredArticles" :key="a.id">
    {{ a.title }}
  </div>
</template>

watch vs watchEffect Comparison

FeaturewatchwatchEffect
Whether data source is specifiedManually specifiedAutomatically tracks dependencies
Initial executionBy default doesn't execute (configurable with immediate)Executes immediately
Getting old valueCan get the old valueCannot get the old value
Applicable scenariosWhen you need to compare old and new values, precisely control timingWhen you don't care about old/new values, just want to "respond to changes"

Simple selection guide: If your logic is "when X changes, do Y," use watch. If your logic is "as long as any dependency changes, recompute the result," use watchEffect or computed. In practice, data that can be computed should use computed first; watch is for handling "side effects"—like calling APIs, saving locally, manipulating the DOM.


Debounce Approach

Every keystroke in the search box triggers filtering. That's fine for local data, but if every input sends a network request, it wastes a lot.

DebounceThe idea: wait until the user stops typing for a period, then execute the operation.

Example

import { ref, watch } from 'vue'

const keyword = ref('')
const filteredArticles = ref([])

let timer = null   // Timer ID

watch(keyword, (newVal) => {
  // Clear the previous timer
  clearTimeout(timer)
  // Only execute filtering if there's no new input change within 300ms
  timer = setTimeout(() => {
    filteredArticles.value = articles.value.filter(
      a => a.title.includes(newVal) || a.summary.includes(newVal)
    )
  }, 300)
})

For this blog project, the data is local, so debouncing isn't necessary. But understanding this approach is very helpful for larger projects later.


Hands-on: Add Real-time Search to the Blog

Add a search box to the NavBar component to filter article titles and summaries in real time by keyword.

Example

<!-- File path: src/views/HomeView.vue -->
<script setup>
import { ref, computed, onMounted } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'

const articles = ref([]) // All articles
const isLoading = ref(true)
const activeCategory = ref('All')
const keyword = ref('') // Search keyword

// Extract all categories
const categories = computed(() => {
  const cats = articles.value.map(a => a.category)
return ['All', ...new Set(cats)]
})

// computed automatically tracks changes to keyword and activeCategory
// If either changes, the filter result will automatically recompute
const filteredArticles = computed(() => {
  let result = articles.value

// Step 1: Filter by category
if (activeCategory.value !== 'All') {
    result = result.filter(a => a.category === activeCategory.value)
  }

// Step 2: Filter by keyword (search title and summary)
  if (keyword.value.trim()) {
    const kw = keyword.value.trim().toLowerCase()
    result = result.filter(a =>
      a.title.toLowerCase().includes(kw) ||
      a.summary.toLowerCase().includes(kw)
    )
  }

  return result
})

// Switching categories doesn't clear the search; the two work in combination
function handleCategoryChange(cat) {
  activeCategory.value = cat
}

// Load data
onMounted(async () => {
  try {
    const res = await fetch('/posts.json')
    articles.value = await res.json()
  } catch (err) {
console.error('Load failed', err)
  } finally {
    isLoading.value = false
  }
})
</script>

<template>
  <div class="home">
    <!-- Search box -->
    <div class="search-bar">
      <input
       v-model="keyword"
       type="text"
       placeholder="Search article title or summary..."
       class="search-input"
     />
      <span v-if="keyword" class="clear-btn" @click="keyword = ''">✕</span>
    </div>

    <!-- Category filter -->
    <CategoryFilter
     :categories="categories"
     :active-category="activeCategory"
     @update-category="handleCategoryChange"
   />

    <!-- Loading / Empty / Normal -->
    <p v-if="isLoading">Loading...</p>
    <p v-else-if="filteredArticles.length === 0">No matching articles found</p>
    <div v-else class="article-grid">
      <BlogCard
       v-for="article in filteredArticles"
       :key="article.id"
       :id="article.id"
       :title="article.title"
       :summary="article.summary"
       :date="article.date"
       :category="article.category"
     />
    </div>
  </div>
</template>

<style scoped>
.home { max-width: 960px; margin: 0 auto; padding: 20px; }

.search-bar {
  position: relative;
  margin-bottom: 20px;
}

.search-input {
  width: 100%;
  padding: 12px 40px 12px 16px;
  border: 2px solid #eee;
  border-radius: 8px;
  font-size: 15px;
  outline: none;
  transition: border-color 0.2s;
}

.search-input:focus {
  border-color: #427DB8;
}

.clear-btn {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  color: #999;
  font-size: 18px;
}

.article-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
  margin-top: 20px;
}
</style>

Now search and category filtering work together—after entering a search keyword, clicking a category button will stack the filters. Both conditions are reactive; when either changes, filteredArticles updates automatically.


Chapter Summary

In this chapter, you mastered watch (manually specify the data source, can get old and new values) and watchEffect (automatically tracks dependencies, executes immediately), and learned to use computed for combined search + category filtering.

The real-time filtering experience of a search box is much smoother than "type and click a button"—this is the value of a reactive system.

Other Extensions