DEV Community

Cover image for Effective Techniques to Improve Search Input in React Applications with Large Data Sets
Gabriel Domingues
Gabriel Domingues

Posted on

Effective Techniques to Improve Search Input in React Applications with Large Data Sets

Handling search functionality in a React application that interacts with an API and a database containing millions of records can be quite challenging. Optimizing both the frontend and backend to manage this large dataset efficiently is crucial for a smooth user experience. In this blog post, I'll share my journey and strategies to enhance search performance, both through a custom-built solution and by leveraging specialized search engines like Algolia, Meilisearch, or Elasticsearch.

Custom-Built Search Solution

Frontend Optimization

Debounce Search Input

Debouncing limits the number of API calls made as the user types, reducing the load on both the client and server. Imagine a user rapidly typing in a search bar; without debouncing, each keystroke triggers an API call, quickly overwhelming the server and creating a sluggish user experience.

import { useState, useCallback, ChangeEvent } from 'react';
import { debounce } from "lodash";

const SearchComponent: React.FC = () => {
  const [query, setQuery] = useState<string>('');

  const handleSearch = useCallback(
    debounce((searchTerm: string) => {
      // API call logic
    }, 300),
    []
  );

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
    handleSearch(e.target.value);
  };

  return <input type="text" value={query} onChange={handleChange} />;
};
Enter fullscreen mode Exit fullscreen mode

In this example, handleSearch is only called 300 milliseconds after the user stops typing, reducing the number of API requests.

Pagination and Infinite Scroll

Loading all results at once can be inefficient and slow, especially with large datasets. Instead, implement pagination or infinite scroll to load data in manageable chunks. Imagine a user searching for "shoes" on an e-commerce site; with pagination, the results are displayed page by page, improving load times and user experience.

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

interface DataItem {
  id: string;
  name: string;
}

const PaginatedSearch: React.FC = () => {
  const [data, setData] = useState<DataItem[]>([]);
  const [page, setPage] = useState<number>(1);
  const [query, setQuery] = useState<string>('');

  const fetchData = async () => {
    const response = await fetch(`/api/search?query=${query}&page=${page}`);
    const result = await response.json();
    setData((prevData) => [...prevData, ...result.results]);
  };

  useEffect(() => {
    fetchData();
  }, [page, query]);

  return (
    <div>
      <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} />
      {data.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
      <button onClick={() => setPage(page + 1)}>Load More</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, each click on "Load More" fetches the next page of results, enhancing the user experience by loading data as needed.

Client-Side Caching

Implement caching strategies to store previous search results and reduce redundant API calls. This is akin to having a memory of past searches, speeding up repeated queries and improving the overall efficiency.

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then((res) => res.json());

const SearchComponent: React.FC = () => {
  const { data, error } = useSWR('/api/search?query=example', fetcher);

  if (error) return <div>Failed to load</div>;
  if (!data) return <div>Loading...</div>;

  return (
    <div>
      {data.results.map((item: { id: string; name: string }) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here, useSWR handles caching and revalidation, ensuring that repeated searches are quick and efficient.

Backend Optimization

Indexing

Ensure the database is properly indexed, particularly on columns that are frequently searched. Indexes act like a book's index, allowing the database to quickly locate the relevant data.

CREATE INDEX idx_name ON table_name(column_name);
Enter fullscreen mode Exit fullscreen mode

Full-Text Search

Utilize full-text search capabilities of the database to enhance search performance. Full-text search allows for more complex querying and faster search operations.

-- PostgreSQL example
CREATE INDEX idx_gin ON table_name USING gin(to_tsvector('english', column_name));
Enter fullscreen mode Exit fullscreen mode

Query Optimization

Optimize database queries to minimize execution time. Use efficient query plans to ensure quick data retrieval.

EXPLAIN ANALYZE SELECT * FROM table_name WHERE column_name LIKE '%search_term%';
Enter fullscreen mode Exit fullscreen mode

Caching Layer

Implement a caching layer to store frequent search queries and results. This reduces the load on the database and speeds up response times.

import Redis from 'ioredis';

const cache = new Redis();

const searchHandler = async (req, res) => {
  const { query } = req.query;
  const cachedResults = await cache.get(query);

  if (cachedResults) {
    return res.json(JSON.parse(cachedResults));
  }

  const results = await databaseSearch(query);
  await cache.set(query, JSON.stringify(results), 'EX', 3600); // Cache for 1 hour

  res.json(results);
};
Enter fullscreen mode Exit fullscreen mode

API Rate Limiting and Throttling

Implement rate limiting to prevent abuse and ensure the system can handle the load efficiently.

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // Limit each IP to 100 requests per windowMs
});

app.use('/api/', limiter);
Enter fullscreen mode Exit fullscreen mode

Infrastructure Optimization

Load Balancing

Distribute incoming requests across multiple servers to balance the load and enhance performance.

upstream api_servers {
  server api1.example.com;
  server api2.example.com;
}

server {
  location /api/ {
    proxy_pass http://api_servers;
  }
}
Enter fullscreen mode Exit fullscreen mode

Horizontal Scaling

Scale the database horizontally to distribute the load across multiple instances or shards.

-- Example of a sharding strategy
CREATE TABLE shard1.table_name (...);
CREATE TABLE shard2.table_name (...);
Enter fullscreen mode Exit fullscreen mode

Leveraging Specialized Search Engines

While a custom-built solution offers tailored optimizations, using specialized search engines like Algolia, Meilisearch, or Elasticsearch can significantly enhance search performance with minimal effort. These services provide powerful, scalable, and feature-rich search capabilities out-of-the-box.

Conclusion

When building a React application that handles search functionality for millions of records, optimizing both the frontend and backend is crucial to providing a seamless user experience. In this post, we explored various strategies to achieve this:

  1. Frontend Optimization: Techniques such as debouncing, pagination, infinite scroll, and client-side caching help minimize API calls, manage large datasets efficiently, and ensure a smooth user experience.

  2. Backend Optimization: Indexing, full-text search, query optimization, caching layers, and API rate limiting enhance database performance and reduce server load.

  3. Infrastructure Optimization: Load balancing and horizontal scaling ensure the system can handle high traffic and large datasets effectively.

  4. Specialized Search Engines: Leveraging services like Algolia, Meilisearch, or Elasticsearch can significantly boost search performance with their advanced features, scalability, and ease of integration. These services provide powerful search capabilities out-of-the-box, allowing developers to focus on core features while ensuring fast, relevant search results.

Top comments (0)