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

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状态在本地过滤数据——这就是你提到的部分水合:只有需要交互的组件会被水合,其他部分保持服务端渲染的静态内容,兼顾性能和交互性。

  1. 创建客户端组件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>
  );
}
  1. 修改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发起无刷新的服务端查询,直接返回筛选后的结果,避免大量数据传输到客户端。

  1. 在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 [];
  }
}
  1. 修改客户端组件,调用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>
  );
}
  1. 简化page.js:
// page.js
import ClientTableWithFilters from "@/components/ClientTableWithFilters";

export default function Home() {
    return <ClientTableWithFilters />;
}

内容的提问来源于stack exchange,提问作者Frizzant

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 18:35:08