You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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'声明),接管前端交互。

修改步骤

  1. 创建客户端组件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>
        </>
    );
}
  1. 修改原页面组件(服务端组件),引入客户端组件并传递数据:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 12:01:01