I have a Next.js client component which shows a data table and has a search bar which can be used to filter the table. The issue is that whenever I type in the search bar, it causes the whole component to re-render, which means the search loses focus and interrupts typing. I have the search debounced but it still has an effect for longer queries.
'use client'
import { useEffect, useState } from "react";
import { PartsTable as PartsRow } from "@/app/lib/definitions";
import PartsTable from "@/app/ui/parts/table";
import NoQueryParamSearch from "../no-query-param-search";
export default function PartSearch({
onPartSelected
}: {
onPartSelected?: any
}) {
const [parts, setParts] = useState([] as PartsRow[]);
const [search, setSearch] = useState('');
useEffect(() => {
fetch(`/api/parts?query=${search}`).then(async (response) => {
const parts = await response.json();
setParts(parts);
});
}, [search]);
const handlePartRowClicked = (part: any) => {
onPartSelected(part);
}
const handleQueryChanged = (query: string) => {
setSearch(query);
}
return (
<div>
<NoQueryParamSearch placeholder="Search ID, description, year, etc..." onChange={handleQueryChanged} />
<div className="border border-solid border-gray-200 rounded-md my-2">
<PartsTable parts={parts.slice(0, 5)} onRowClick={handlePartRowClicked} disableControls />
</div>
</div>
);
}
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useDebouncedCallback } from 'use-debounce';
export default function NoQueryParamSearch({ placeholder, onChange, autoComplete = true }: { placeholder: string, onChange: any, autoComplete?: boolean }) {
const handleSearch = useDebouncedCallback((term) => {
onChange(term);
}, 300);
return (
<div className="relative flex flex-shrink-0">
<label htmlFor="search" className="sr-only">
Search
</label>
<input
className="peer block w-96 rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
autoComplete={autoComplete ? undefined : 'off'}
autoCorrect={autoComplete ? undefined : 'off'}
/>
<MagnifyingGlassIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
);
}
'use client'
import { PartsTable } from '@/app/lib/definitions';
import Search from '@/app/ui/search';
import { useRouter } from 'next/navigation'
import { Button } from '@/app/ui/button';
export default async function PartsTable({
parts,
disableControls,
onRowClick
}: {
parts: any[],
disableControls?: boolean,
onRowClick?: any
}) {
const router = useRouter();
const handleAddNewClick = () => {
router.push(`/dashboard/inventory/add`);
}
const handleRowClick = (part: any) => {
onRowClick(part);
}
return (
<div className="w-full">
{disableControls || (
<div className='flex'>
<Button className='mr-5' onClick={() => handleAddNewClick()}>
Add new +
</Button>
<Search placeholder="Search parts..." />
</div>
)}
<div className="mt-6 flow-root">
<div className="overflow-x-auto">
<div className="inline-block min-w-full align-middle">
<div className="overflow-hidden rounded-md bg-gray-50 p-2 md:pt-0">
<table className="hidden min-w-full rounded-md text-gray-900 md:table">
<thead className="rounded-md bg-gray-50 text-left text-sm font-normal">
<tr>
<th scope="col" className="px-4 py-5 font-bold sm:pl-6">
Stock Code
</th>
<th scope="col" className="px-3 py-5 font-bold">
Description
</th>
<th scope="col" className="px-3 py-5 font-bold">
Order Form Group
</th>
<th scope="col" className="px-3 py-5 font-bold">
Selling Price
</th>
<th scope="col" className="px-3 py-5 font-bold">
Manufacturer
</th>
<th scope="col" className="px-3 py-5 font-bold">
Stock Year
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-900">
{parts.map((part) => (
<tr key={part.id} className="group cursor-pointer hover:text-blue-600 hover:bg-sky-100" onClick={() => handleRowClick(part)}>
<td className="whitespace-nowrappy-5 pl-4 pr-3 text-sm group-first-of-type:rounded-md group-last-of-type:rounded-md sm:pl-6">
<div className="flex items-center gap-3">
<p>{part.stock_code}</p>
</div>
</td>
<td className="whitespace-nowrap px-4 py-5 text-sm">
{part.description}
</td>
<td className="whitespace-nowrap px-4 py-5 text-sm">
{part.order_form_group}
</td>
<td className="whitespace-nowrap px-4 py-5 text-sm">
{(part.selling_price_cents / 100).toLocaleString("en-US", { style: "currency", currency: "USD" })}
</td>
<td className="whitespace-nowrap px-4 py-5 text-sm">
{part.manufacturer}
</td>
<td className="whitespace-nowrap px-4 py-5 text-sm group-first-of-type:rounded-md group-last-of-type:rounded-md">
{part.stock_year}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}
This needs to be a client component because of onPartSelected
. When a row is selected it needs to send that to the parent component.
2
Answers
Make a parent component and put Search and Parts components in it separately, so changing state rendered in Parts would affect only that component, and not Search.
States would be kept in parent component, I assume.
EDIT: Ok, I got it working this way. Use separate Parts component, provide it with search term and on search term change, get the api data from that component (adapt it to your code, this is just an example that worked for me):
Try making the text field in NoQueryParamSearch stateful, parent state updates shouldn’t affect the focus state of child components unless you are specifically moving the focus.