Next.js 14服务端组件实现赛事结果表格过滤方案咨询
问题与解决方案
问题背景
熟悉Next.js旧架构,新项目采用App Router的服务端/客户端组件新架构。需要实现一个满足SEO需求、服务端渲染的赛事结果表格,页面加载后添加输入框供用户筛选运动员。尝试旧架构的getServerSideProps方法无效,寻求可行方案。
当前页面代码:
const RaceResultsPage = async ({ params }: { params: { raceId: string } }) => { const result = await getResultByRaceId(+params.raceId); return ( <> <div> {/* The input I want to use to filter the table*/} <div className="relative mt-2 rounded-md shadow-sm"> <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" /> </div> <input type="email" name="email" id="email" className="block w-full rounded-md border-0 py-1.5 pl-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" placeholder="Filtrer par nom, prénom, numéro, club" /> </div> </div> <div className="mt-8 flow-root"> <div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="inline-block min-w-full py-2 align-middle"> <table className="min-w-full divide-y divide-gray-300"> <thead> <tr> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Pos. </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Bib. </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Nom </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Cat </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Temps </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Ecart </th> </tr> </thead> <tbody className="divide-y divide-gray-200 bg-white"> {result.map((person) => ( <tr key={person.bib}> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.pos}</td> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.bib}</td> <td className="whitespace-nowrap px-3 py-4 text-sm font-medium text-gray-900"> {person.name} </td> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.catRank}</td> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.totalTime}</td> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.diff}</td> </tr> ))} </tbody> </table> </div> </div> </div> </> ); };
解决方案
核心原理
App Router中默认页面组件是服务端组件,无法使用useState、事件处理等客户端交互API,这是旧架构方法失效的原因。需拆分组件:
- 页面本身保留为服务端组件,负责服务端获取数据、渲染初始表格,满足SEO需求。
- 将筛选输入框和交互逻辑封装为客户端组件(通过
'use client'声明),接管前端交互。
修改步骤
- 创建客户端组件
FilterableTable.tsx,处理筛选逻辑:
'use client'; import { useState } from 'react'; import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'; // 定义结果数据类型,根据实际情况调整 type RaceResult = { pos: string; bib: string; name: string; catRank: string; totalTime: string; diff: string; }; export default function FilterableTable({ results }: { results: RaceResult[] }) { // 维护筛选关键词状态 const [filterKeyword, setFilterKeyword] = useState(''); // 实时过滤结果 const filteredResults = results.filter(person => { const lowerKeyword = filterKeyword.toLowerCase(); return ( person.name.toLowerCase().includes(lowerKeyword) || person.bib.includes(lowerKeyword) || person.catRank.toLowerCase().includes(lowerKeyword) // 可扩展添加俱乐部等其他筛选字段 ); }); return ( <> <div> <div className="relative mt-2 rounded-md shadow-sm"> <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" /> </div> <input type="text" {/* 修正输入框类型为text,符合筛选场景 */} name="filter" id="filter" className="block w-full rounded-md border-0 py-1.5 pl-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" placeholder="Filtrer par nom, prénom, numéro, club" value={filterKeyword} onChange={(e) => setFilterKeyword(e.target.value)} /> </div> </div> <div className="mt-8 flow-root"> <div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="inline-block min-w-full py-2 align-middle"> <table className="min-w-full divide-y divide-gray-300"> <thead> <tr> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Pos. </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Bib. </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Nom </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Cat </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Temps </th> <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> Ecart </th> </tr> </thead> <tbody className="divide-y divide-gray-200 bg-white"> {filteredResults.map((person) => ( <tr key={person.bib}> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.pos}</td> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.bib}</td> <td className="whitespace-nowrap px-3 py-4 text-sm font-medium text-gray-900"> {person.name} </td> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.catRank}</td> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.totalTime}</td> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.diff}</td> </tr> ))} </tbody> </table> </div> </div> </div> </> ); }
- 修改原页面组件(服务端组件),引入客户端组件并传递数据:
import FilterableTable from './FilterableTable'; const RaceResultsPage = async ({ params }: { params: { raceId: string } }) => { const result = await getResultByRaceId(+params.raceId); return ( <> <FilterableTable results={result} /> </> ); }; export default RaceResultsPage;
关键说明
- 服务端渲染的完整赛事结果会被搜索引擎正常抓取,满足SEO需求。
- 页面加载后,客户端组件接管交互,用户输入时通过
useState实时更新筛选结果,无需额外请求。 - 输入框类型从
email修正为text,符合筛选场景。
内容的提问来源于stack exchange,提问作者Cédric Guérin
相关产品推荐
相关产品推荐

