Next.js客户端组件搜索输入时失焦问题求助
解决Next.js客户端组件搜索时输入框失焦问题
我有一个Next.js客户端组件,用于展示数据表格并提供搜索栏筛选内容。当前遇到的问题是:在搜索栏输入内容时,整个组件会触发重新渲染,导致输入框失去焦点,打断输入流程。虽然已经对搜索功能做了防抖处理,但较长的查询内容仍会受此问题影响。此外,由于需要通过onPartSelected将选中行的数据传递给父组件,该组件必须保持为客户端组件。
问题根源
出现这个问题的核心原因有两个:
PartsTable被定义为异步函数组件(async function PartsTable),但内部没有执行任何异步操作。在Next.js中,异步客户端组件会引发额外的重渲染开销,甚至导致组件树重新挂载,直接造成输入框失焦。- 传递给子组件的回调函数(
handlePartRowClicked、handleQueryChanged)在每次父组件重渲染时都会创建新实例,触发子组件不必要的重渲染。
修复方案
1. 将PartsTable改为普通客户端组件
去掉async关键字,因为组件内部没有异步逻辑(useRouter的使用不需要异步):
'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'; // 移除async关键字 export default 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-nowrap 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> ); }
2. 使用useCallback缓存回调函数
在PartSearch组件中,用useCallback包裹回调函数,确保它们在组件重渲染时保持相同引用:
'use client' import { useEffect, useState, useCallback } 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(''); // 缓存行点击回调 const handlePartRowClicked = useCallback((part: any) => { onPartSelected?.(part); }, [onPartSelected]); // 缓存搜索回调 const handleQueryChanged = useCallback((query: string) => { setSearch(query); }, []); useEffect(() => { // 添加请求取消逻辑,避免竞态问题 const controller = new AbortController(); const signal = controller.signal; fetch(`/api/parts?query=${search}`, { signal }) .then(async (response) => { if (!response.ok) throw new Error('请求失败'); const parts = await response.json(); setParts(parts); }) .catch(err => { if (err.name !== 'AbortError') console.error('获取数据失败:', err); }); return () => controller.abort(); }, [search]); 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> ); }
3. 可选:优化搜索组件的防抖逻辑
确保防抖回调的引用稳定,进一步减少不必要的重渲染:
'use client'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { useDebouncedCallback } from 'use-debounce'; import { useCallback } from 'react'; export default function NoQueryParamSearch({ placeholder, onChange, autoComplete = true }: { placeholder: string, onChange: any, autoComplete?: boolean }) { const handleSearch = useDebouncedCallback( useCallback((term) => { onChange(term); }, [onChange]), 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> ); }
效果说明
修改完成后,组件的重渲染会被大幅减少,输入框不会再因重渲染而失焦。同时添加的请求取消逻辑可以避免旧请求覆盖新请求的竞态问题,提升整体用户体验。
内容的提问来源于stack exchange,提问作者Austin
相关产品推荐
相关产品推荐

