Next.js 14表格动态筛选:无页面重载的实现方案
问题描述
我有一个客户端数据表格,单行内容可通过编辑按钮修改,现在需要在表头的黄色区域添加筛选功能(比如在姓氏栏输入"Joh"时,展示所有匹配的条目)。
现有page.js代码如下:
/** page.js **/ import connection from "/lib/db"; import {format} from "date-fns"; import Image from "next/image"; import ClientTableRow from "@/components/ClientTableRow"; export default async function Home() { /** * @type {{id: number, name: string, last_name: string, phone: string, email: string, created_at: string}[]} */ let data = []; try { const [rows] = await connection.query('SELECT * FROM clients'); data = rows.map(row => ({ ...row, created_at: format(new Date(row.created_at), 'dd-MM-yyyy HH:mm:ss') })); } catch (error) { console.error('Error fetching data:', error.message); } return ( <table className="min-w-full divide-y divide-gray-300"> <thead className="bg-gray-50"> <tr> {/*filter should be here*/} <td>ID</td> <td>Name</td> <td>Last name</td> <td>Phone</td> <td>Email</td> <td>Created at</td> <td></td> </tr> </thead> <tbody className="divide-y divide-gray-200 bg-white"> {data.map((item, index) => ( <ClientTableRow key={index} client={item}/> ))} </tbody> </table> ); }
我还有actions.js和ClientTableRow.js文件,暂无需提供其代码。
核心问题:如何在不重载页面的情况下动态更新表格内容?我知道纯React的实现方式,但不确定在Next.js中该如何处理,推测可能用到部分水合(partial rehydration)和Server Actions技术?
解决方案
方案一:客户端本地过滤(适合数据量不大的场景)
你的page.js是服务端组件,没法直接处理输入框这类客户端交互,所以得把筛选逻辑拆成客户端组件,用React状态在本地过滤数据——这就是你提到的部分水合:只有需要交互的组件会被水合,其他部分保持服务端渲染的静态内容,兼顾性能和交互性。
- 创建客户端组件
ClientTableWithFilters.js,标记'use client':
// components/ClientTableWithFilters.js 'use client'; import { useState } from 'react'; import ClientTableRow from './ClientTableRow'; export default function ClientTableWithFilters({ initialData }) { const [lastNameFilter, setLastNameFilter] = useState(''); // 实时计算过滤后的数据 const filteredData = initialData.filter(item => item.last_name.toLowerCase().includes(lastNameFilter.toLowerCase()) ); return ( <table className="min-w-full divide-y divide-gray-300"> <thead className="bg-gray-50"> <tr> <td>ID</td> <td>Name</td> <td> Last name {/* 姓氏筛选输入框 */} <input type="text" value={lastNameFilter} onChange={(e) => setLastNameFilter(e.target.value)} placeholder="筛选姓氏..." className="ml-2 p-1 border rounded-sm text-sm" /> </td> <td>Phone</td> <td>Email</td> <td>Created at</td> <td></td> </tr> </thead> <tbody className="divide-y divide-gray-200 bg-white"> {filteredData.map((item, index) => ( <ClientTableRow key={index} client={item} /> ))} </tbody> </table> ); }
- 修改
page.js,引入组件并传递初始数据:
// page.js import connection from "/lib/db"; import {format} from "date-fns"; import ClientTableWithFilters from "@/components/ClientTableWithFilters"; export default async function Home() { let data = []; try { const [rows] = await connection.query('SELECT * FROM clients'); data = rows.map(row => ({ ...row, created_at: format(new Date(row.created_at), 'dd-MM-yyyy HH:mm:ss') })); } catch (error) { console.error('Error fetching data:', error.message); } return <ClientTableWithFilters initialData={data} />; }
方案二:Server Actions + 服务端过滤(适合大数据量场景)
如果数据量很大,本地过滤性能差,可以用Server Actions发起无刷新的服务端查询,直接返回筛选后的结果,避免大量数据传输到客户端。
- 在
actions.js中定义筛选的Server Action(注意用参数化查询防SQL注入):
// actions.js 'use server'; import connection from "/lib/db"; import {format} from "date-fns"; export async function filterClients(lastName) { try { let query = 'SELECT * FROM clients'; let params = []; if (lastName) { query += ' WHERE last_name LIKE ?'; params.push(`%${lastName}%`); } const [rows] = await connection.query(query, params); return rows.map(row => ({ ...row, created_at: format(new Date(row.created_at), 'dd-MM-yyyy HH:mm:ss') })); } catch (error) { console.error('Error filtering clients:', error.message); return []; } }
- 修改客户端组件,调用Server Action获取数据:
// components/ClientTableWithFilters.js 'use client'; import { useState, useEffect } from 'react'; import ClientTableRow from './ClientTableRow'; import { filterClients } from '../actions'; export default function ClientTableWithFilters() { const [lastNameFilter, setLastNameFilter] = useState(''); const [data, setData] = useState([]); const [isLoading, setIsLoading] = useState(false); // 初始加载全量数据 useEffect(() => { const fetchInitialData = async () => { setIsLoading(true); const result = await filterClients(''); setData(result); setIsLoading(false); }; fetchInitialData(); }, []); // 筛选变化时调用Server Action const handleFilterChange = async (e) => { const value = e.target.value; setLastNameFilter(value); setIsLoading(true); const result = await filterClients(value); setData(result); setIsLoading(false); }; return ( <table className="min-w-full divide-y divide-gray-300"> <thead className="bg-gray-50"> <tr> <td>ID</td> <td>Name</td> <td> Last name <input type="text" value={lastNameFilter} onChange={handleFilterChange} placeholder="筛选姓氏..." className="ml-2 p-1 border rounded-sm text-sm" disabled={isLoading} /> </td> <td>Phone</td> <td>Email</td> <td>Created at</td> <td></td> </tr> </thead> <tbody className="divide-y divide-gray-200 bg-white"> {isLoading ? ( <tr><td colSpan="7" className="text-center py-4">加载中...</td></tr> ) : ( data.map((item, index) => ( <ClientTableRow key={index} client={item} /> )) )} </tbody> </table> ); }
- 简化
page.js:
// page.js import ClientTableWithFilters from "@/components/ClientTableWithFilters"; export default function Home() { return <ClientTableWithFilters />; }
内容的提问来源于stack exchange,提问作者Frizzant
相关产品推荐
相关产品推荐

