Ever built a dropdown that crashes your browser or hammers your database because it fetches results on every single keystroke? You need a searchable dropdown that waits for the user to stop typing before making a network request.
- How to set up a Laravel backend endpoint for searching records
- How to use lodash debounce in a React component via Inertia.js
- How to manage dropdown open, close, and selection states
- How to handle keyboard and mouse interactions smoothly
1. The Laravel Backend Route and Controller
We need an API endpoint that takes a search query and returns matching results from the database. We will use Laravel's query builder to filter users by name or email.
use App\Models\User;\nuse Illuminate\Http\Request;\n\npublic function search(Request $request)\n{\n $query = $request->input('query');\n\n $users = User::query()\n ->when($query, function ($q) use ($query) {\n $q->where('name', 'like', "%{$query}%")\n ->orWhere('email', 'like', "%{$query}%");\n })\n ->limit(10)\n ->get();\n\n return response()->json($users);\n}Limiting the results to ten prevents your dropdown list from overflowing the user interface and keeps queries fast.
2. The React Component with Debounce
Next, we build the React component using Inertia.js. We use a technique called debouncing, which delays our search function until the user stops typing for 300 milliseconds.
import React, { useState, useEffect, useMemo } from 'react';\nimport axios from 'axios';\nimport debounce from 'lodash/debounce';\n\nexport default function SearchableDropdown({ onSelect }) {\n const [query, setQuery] = useState('');\n const [results, setResults] = useState([]);\n const [isOpen, setIsOpen] = useState(false);\n\n const fetchResults = useMemo(() => debounce(async (searchQuery) => {\n if (!searchQuery) {\n setResults([]);\n return;\n }\n const response = await axios.get(`/api/users/search?query=${searchQuery}`);\n setResults(response.data);\n }, 300), []);\n\n useEffect(() => {\n fetchResults(query);\n }, [query, fetchResults]);\n\n return (\n <div className="relative">\n <input\n type="text"\n value={query}\n onChange={(e) => { setQuery(e.target.value); setIsOpen(true); }}\n placeholder="Search users..."\n className="w-full px-3 py-2 border rounded"\n />\n {isOpen && results.length > 0 && (\n <ul className="absolute z-10 w-full bg-white border rounded mt-1 shadow-lg">\n {results.map((user) => (\n <li\n key={user.id}\n onClick={() => { onSelect(user); setQuery(user.name); setIsOpen(false); }}\n className="px-3 py-2 hover:bg-gray-100 cursor-pointer"\n >\n {user.name}</li>\n ))}\n </ul>\n )}</div>\n );\n}Wrapping our search function in useMemo ensures the debounce timer does not reset on every single component re-render.
Common mistakes
A frequent mistake is forgetting to cancel the debounce timer when the component unmounts, which can cause memory leaks. Another issue is failing to clear the results array when the input query becomes completely empty, leaving stale search suggestions visible on the screen.
Try adding this component to your current Laravel and Inertia project today, and hook it up to your own database models.