Every time a user types in a search box, your app might fire an API request on every single keystroke. This quickly hammers your server and slows down the user interface.

You can fix this by building a searchable dropdown with debounce, which is a programming practice that delays function execution until the user stops typing for a specific moment. In this tutorial, you will build this exact component, package it in a Docker container, and deploy it for free on Render.

  • Write a custom debounce hook in React to limit API calls
  • Create a clean, accessible searchable dropdown component
  • Containerize the frontend app using Docker
  • Deploy the production container live to Render via automated CI/CD

Building the Debounce Hook and Dropdown

First, let's write a custom React hook called useDebounce. This hook takes your search query and a delay time, and only returns the updated value after the user stops typing.

import { useState, useEffect } from 'react';

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

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

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

This snippet sets a timer that clears itself every time the user types a new character, ensuring the value only updates once they pause.

Next, let's use this hook inside our searchable dropdown component. This component fetches matching items from an API only after the user pauses their typing.

import { useState, useEffect } from 'react';
import { useDebounce } from './useDebounce';

export default function SearchDropdown() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (!debouncedQuery) {
      setResults([]);
      return;
    }
    fetch(`https://api.example.com/search?q=${debouncedQuery}`)
      .then(res => res.json())
      .then(data => setResults(data));
  }, [debouncedQuery]);

  return (
    <div className="dropdown">
      <input 
        type="text" 
        value={query} 
        onChange={(e) => setQuery(e.target.value)} 
        placeholder="Search items..." 
      />
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

By passing the debounced query into our useEffect dependency array, we guarantee the fetch request only triggers 300 milliseconds after typing stops.

Dockerizing the Frontend App

To run our app consistently anywhere, we use Docker, a tool that packages your application and its environment into a single container. Create a file named Dockerfile in your project root to define how the container builds.

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

This multi-stage Dockerfile builds your React code and serves the static output using Nginx, which is a fast, lightweight web server.

Render development Photo by Mohammad Rahmani on Unsplash

Deploying to Render via CI/CD

Continuous Integration and Continuous Deployment, or CI/CD, is an automated pipeline that tests and deploys your code whenever you push changes to GitHub. Push your code to a GitHub repository and log into Render, which is a cloud platform for hosting web applications easily.

Create a new Web Service on Render and connect your GitHub repository. Select Docker as the environment, and Render will automatically read your Dockerfile and deploy your app live.

Common mistakes

A frequent mistake when building debounced inputs is forgetting to clear the timeout when the component unmounts, which can cause memory leaks in your application. Always return the cleanup function using clearTimeout as shown in our hook example. Another common slip-up is setting the debounce delay too low, such as 50 milliseconds, which fails to actually reduce the number of server requests.

Clone the starter repository on GitHub right now and try swapping out the mock API endpoint with a real public API like the GitHub user search.