Real-Time Search with useEffect and useState

Published on 15.7.2019

One of the most common features in modern web applications is real-time search. Whether you’re building a to-do list, a product catalog, or a social media app, users expect to see results update as they type. React’s useState and useEffect hooks are perfect for implementing this feature. However, to ensure performance remains optimal, especially when dealing with large datasets, you may need to use debouncing to limit the number of API calls or state updates.

In this post, we’ll walk through how to implement a real-time search feature with live filtering using useEffect and useState. We’ll also cover how to debounce the input to prevent unnecessary processing.


Setting Up the Real-Time Search

Before diving into the code, let’s briefly discuss the scenario. Imagine you have a list of items, and you want the user to filter through that list in real time as they type into an input field. The challenge here is to manage both the input state and the filtered results efficiently.

Let’s start by setting up the basic components and state for the search feature.

import React, { useState, useEffect } from 'react';

const items = [
  'Apple',
  'Banana',
  'Cherry',
  'Date',
  'Grapes',
  'Kiwi',
  'Lemon',
  'Mango',
  'Orange',
  'Peach',
];

function RealTimeSearch() {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);

  useEffect(() => {
    setFilteredItems(
      items.filter((item) => item.toLowerCase().includes(query.toLowerCase()))
    );
  }, [query]);

  const handleChange = (event) => {
    setQuery(event.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Search for fruits..."
      />
      <ul>
        {filteredItems.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default RealTimeSearch;

How This Works:

  1. We create a list of items (items) for the search to filter through.
  2. We use useState to store the current query (query) entered by the user and the filtered list (filteredItems).
  3. The useEffect hook listens for changes in the query state. When the query changes, it filters the list of items and updates the filteredItems state with the results.
  4. The input field updates the query state whenever the user types, triggering the useEffect hook and updating the list in real-time.

Debouncing the Input

While the above implementation works fine, it can lead to performance issues when the list becomes large or when the user types quickly. Each keystroke causes a re-render, and in the case of API calls, it might lead to a flood of requests.

To improve this, we can implement debouncing. Debouncing allows us to delay the update until the user stops typing for a specified amount of time. This reduces the number of updates and ensures better performance.

Let’s add a debounce function to our search:

import React, { useState, useEffect } from 'react';

// Debounce function
const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // Clean up the timeout on value change
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
};

const items = [
  'Apple',
  'Banana',
  'Cherry',
  'Date',
  'Grapes',
  'Kiwi',
  'Lemon',
  'Mango',
  'Orange',
  'Peach',
];

function RealTimeSearch() {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  
  // Apply debounce to the query
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    setFilteredItems(
      items.filter((item) => item.toLowerCase().includes(debouncedQuery.toLowerCase()))
    );
  }, [debouncedQuery]);

  const handleChange = (event) => {
    setQuery(event.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Search for fruits..."
      />
      <ul>
        {filteredItems.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default RealTimeSearch;

How Debouncing Works:

  1. Custom useDebounce Hook:
    • This hook takes a value (query) and a delay (500ms in our case) and returns the debounced value.
    • It uses setTimeout to wait for the specified delay before updating the debounced value. If the user types again within that time, the previous timeout is cleared and reset.
    • This ensures that the search query is only updated after the user stops typing for the given period.
  2. Using Debounced Query:
    • We now use debouncedQuery inside the useEffect hook instead of query. The filtering only occurs when the debounced query has settled, improving performance by reducing the number of re-renders and filter operations.

Why Use Debouncing?

Debouncing is especially useful for optimizing performance when:

  • Making API calls based on user input (to avoid sending a request for every keystroke).
  • Filtering large datasets or performing computationally expensive operations.
  • Improving the overall user experience by providing smooth interactions.

In the example above, debouncing the search input prevents unnecessary filtering as the user types quickly, ensuring the UI remains responsive.


Conclusion

In this post, we’ve built a real-time search feature using React’s useState and useEffect hooks. We also optimized the performance by introducing debouncing with a custom useDebounce hook. Debouncing ensures that we don’t overwhelm the application with too many updates or requests, making the search smoother and more efficient.

By leveraging hooks like useState and useEffect along with debouncing, you can create powerful and responsive real-time search experiences in your React applications.

Happy coding!

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *