如何在Next.js Todo应用中无需刷新页面更新组件数据?
问题:Next.js Todo应用CRUD后客户端数据无法自动更新
我基于Next.js、Prisma、TypeScript和PostgreSQL开发Todo应用,数据通过API目录结合Prisma与数据库交互,Task表的CRUD操作调用http://3000/api/tasks接口,任务列表由Todos组件通过useEffect渲染。当前CRUD操作完成后,客户端展示的数据不会自动更新,必须刷新页面才能显示最新状态。尝试过5秒轮询获取数据,但服务器开销过大,希望实现无需刷新页面即可更新数据的效果。
相关代码
Todos.tsx
export default function Todos({ attribute, value, title, limit, addBtn }: arguments) { const [data, setData] = useState<Item[]>([]); //fetching data useEffect(() => { const fetchData = async () => { const response: any = await obtenerTasks({ attribute, value, limit }); setData(response.response); }; fetchData(); }, []); return ( <div> <div> <h1>{title}</h1> <div className='todos_container'> { data.map((item) => ( <Task key={item.id} id={item.id} title={item.title} description={item.description} createdAt={item.createdAt} category={item.category} completed={item.completed} priority={item.priority} dueDate={item.dueDate} /> )) } </div> </div> {addBtn && ( <div className='todo_showMore'> <Link href={`/task/${attribute}?value=${value}`}> <p>ver mas</p> </Link> </div> )} </div> ); }
lib/task/api_task.ts
//get tasks const obtenerTasks = async ({ attribute, value, limit }: TaskProps): Promise<any> => { try { let url: string = `http://localhost:3000/api/tasks?attribute=${attribute}&value=${value}`; if (limit) url += `&limit=${limit}`; const response = await axios.get(url); return { response: response.data, status: response.status } } catch (error) { throw new Error(String(error)); } }; //create task const crearTask = async (task: Task): Promise<any> => { try { const url: string = "http://localhost:3000/api/tasks"; const response = await axios.post(url, task); return { response: response.data, status: response.status } } catch (error) { throw new Error(String(error)); } }; //update task const actualizarTask = async (task: Task): Promise<any> => { try { const url: string = `http://localhost:3000/api/tasks?id=${task.id}`; const response = await axios.put(url, task); return { response: response.data, status: response.status } } catch (error) { throw new Error(String(error)); } }; //delete taks const eliminarTask = async (id: number): Promise<any> => { try { const url: string = `http://localhost:3000/api/tasks?id=${id}`; const response = await axios.delete(url); return { response: response.data, status: response.status } } catch (error) { throw new Error(String(error)); } };
Home.tsx
<div className="container_todos"> <Todos attribute={"completed"} value={"false"} title={"Pendiente"} limit={3} addBtn={true} /> <Todos attribute={"completed"} value={"true"} title={"Completado"} limit={3} addBtn={true} /> <Todos attribute={"priority"} value={"true"} title={"Prioridad"} limit={3} addBtn={true} /> </div>
/api/task/route.ts
import prisma from '../../../../prisma/client/index'; import { NextResponse } from 'next/server'; //objeto todo con todos sus atributos type Todo = { title: string, description?: string, category?: string, createdAt?: string, dueDate?: string, priority?: boolean, completed?: boolean, } //metodo GET que obtiene los Task que se filtran en base a sus atributos export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); //obtener parametros url const attribute = searchParams.get('attribute'); //obtener el atributo de la URL a buscar const value = searchParams.get('value'); //obtener el valor del atributo const limit = searchParams.get('limit'); //obtener el valor del atributo let isTrue = (value == "true") ? true : false; //variable auxiliar para consultar clausula let whereClause; //inicializacion de clausula let todos; //clausula de consulta a la base de datos if(attribute && value) { whereClause = { [attribute as string]: value }; } //obtener los Task completados o marcados como prioridad if(attribute == "completed" || attribute == "priority") { whereClause = { [attribute as string]: isTrue }; } //query de consulta a la base de datos if (limit) { const parsedLimit = parseInt(limit, 10); if (!isNaN(parsedLimit)) { todos = await prisma.todo.findMany({ where: whereClause, take: parsedLimit }); } else { todos = await prisma.todo.findMany({ where: whereClause }); } } else { todos = await prisma.todo.findMany({ where: whereClause }); } return NextResponse.json(todos); } catch (error) { console.log(error); return NextResponse.json({ error: 'Internal server error' }); } } //metodo POST que crea un nuevo task export async function POST(request: Request) { try { const data:Todo = await request.json(); const {title, description, category, createdAt, dueDate, priority, completed} = data; const todo = await prisma.todo.create({ data: { title, description, category, createdAt, dueDate, priority, completed, }, }); return NextResponse.json(todo); } catch (error) { console.log(error) } } export async function PUT(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); const data: Todo = await request.json(); const { title, description, category, createdAt, dueDate, priority, completed } = data; try { if (id) { const updatedTodo = await prisma.todo.update({ where: { id: parseInt(id) }, data: { title, description, category, createdAt, dueDate, priority, completed, }, }); return NextResponse.json(updatedTodo); } else { console.log('Param "id" is not present at URL'); } } catch (error) { console.log(error); } } export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); try { await prisma.todo.delete({ where: {id: parseInt(id as string) } }); return NextResponse.json({ msg: `Todo ID ${id} deleted successfully` }); } catch (error) { console.log(error); } }
解决方案
方案1:CRUD操作后手动更新本地状态
在执行创建、更新、删除操作的回调中,直接修改组件的本地状态,无需重新请求整个列表。
步骤1:将状态提升到父组件(Home.tsx)
'use client'; import { useState, useEffect } from 'react'; import Todos from './Todos'; import { obtenerTasks } from './lib/task/api_task'; export default function Home() { const [pendingTasks, setPendingTasks] = useState([]); const [completedTasks, setCompletedTasks] = useState([]); const [priorityTasks, setPriorityTasks] = useState([]); // 初始化加载数据 useEffect(() => { const fetchAllTasks = async () => { const pendingRes = await obtenerTasks({ attribute: "completed", value: "false", limit: 3 }); const completedRes = await obtenerTasks({ attribute: "completed", value: "true", limit: 3 }); const priorityRes = await obtenerTasks({ attribute: "priority", value: "true", limit: 3 }); setPendingTasks(pendingRes.response); setCompletedTasks(completedRes.response); setPriorityTasks(priorityRes.response); }; fetchAllTasks(); }, []); // 定义状态更新函数 const updatePending = (newTasks) => setPendingTasks(newTasks); const updateCompleted = (newTasks) => setCompletedTasks(newTasks); const updatePriority = (newTasks) => setPriorityTasks(newTasks); return ( <div className="container_todos"> <Todos attribute={"completed"} value={"false"} title={"Pendiente"} limit={3} addBtn={true} tasks={pendingTasks} updateTasks={updatePending} /> <Todos attribute={"completed"} value={"true"} title={"Completado"} limit={3} addBtn={true} tasks={completedTasks} updateTasks={updateCompleted} /> <Todos attribute={"priority"} value={"true"} title={"Prioridad"} limit={3} addBtn={true} tasks={priorityTasks} updateTasks={updatePriority} /> </div> ); }
步骤2:修改Todos组件处理状态更新
export default function Todos({ attribute, value, title, limit, addBtn, tasks, updateTasks }: arguments) { // 处理任务更新 const handleTaskUpdate = async (updatedTask) => { await actualizarTask(updatedTask); // 替换列表中的对应任务 updateTasks(tasks.map(task => task.id === updatedTask.id ? updatedTask : task)); }; // 处理任务删除 const handleTaskDelete = async (taskId) => { await eliminarTask(taskId); // 过滤掉已删除的任务 updateTasks(tasks.filter(task => task.id !== taskId)); }; return ( <div> <div> <h1>{title}</h1> <div className='todos_container'> { tasks.map((item) => ( <Task key={item.id} id={item.id} title={item.title} description={item.description} createdAt={item.createdAt} category={item.category} completed={item.completed} priority={item.priority} dueDate={item.dueDate} onUpdate={handleTaskUpdate} onDelete={handleTaskDelete} /> )) } </div> </div> {addBtn && ( <div className='todo_showMore'> <Link href={`/task/${attribute}?value=${value}`}> <p>ver mas</p> </Link> </div> )} </div> ); }
步骤3:创建任务后更新对应列表
在创建任务的组件中,完成创建后更新对应状态:
'use client'; import { useState } from 'react'; import { crearTask } from '../lib/task/api_task'; export default function CreateTask({ onTaskCreated }) { const [title, setTitle] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); const newTask = { title, completed: false, priority: false }; const res = await crearTask(newTask); // 通知父组件更新待办列表 onTaskCreated(res.response); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="任务标题" /> <button type="submit">添加任务</button> </form> ); }
方案2:使用SWR实现自动重新验证
SWR支持缓存和自动重新获取数据,CRUD操作后触发重新验证即可更新UI。
安装SWR
npm install swr
修改Todos组件
'use client'; import useSWR from 'swr'; import { obtenerTasks, actualizarTask, eliminarTask } from '../lib/task/api_task'; export default function Todos({ attribute, value, title, limit, addBtn }: arguments) { const fetcher = async () => { const res = await obtenerTasks({ attribute, value, limit }); return res.response; }; // 用唯一key标识缓存数据 const { data: tasks, error, mutate } = useSWR(`tasks-${attribute}-${value}-${limit}`, fetcher); const handleTaskUpdate = async (updatedTask) => { await actualizarTask(updatedTask); mutate(); // 触发重新请求更新UI }; const handleTaskDelete = async (taskId) => { await eliminarTask(taskId); mutate(); }; if (error) return <div>加载失败</div>; if (!tasks) return <div>加载中...</div>; return ( <div> <div> <h1>{title}</h1> <div className='todos_container'> { tasks.map((item) => ( <Task key={item.id} id={item.id} title={item.title} description={item.description} createdAt={item.createdAt} category={item.category} completed={item.completed} priority={item.priority} dueDate={item.dueDate} onUpdate={handleTaskUpdate} onDelete={handleTaskDelete} /> )) } </div> </div> {addBtn && ( <div className='todo_showMore'> <Link href={`/task/${attribute}?value=${value}`}> <p>ver mas</p> </Link> </div> )} </div> ); }
方案3:使用Next.js Server Actions(App Router)
如果使用Next.js 13+ App Router,可通过Server Actions直接执行数据库操作,操作完成后自动刷新组件状态。
创建Server Actions
'use server'; import prisma from '../../prisma/client/index'; export async function createTask(data: any) { return await prisma.todo.create({ data }); } export async function updateTask(id: number, data: any) { return await prisma.todo.update({ where: { id }, data }); } export async function deleteTask(id: number) { return await prisma.todo.delete({ where: { id } }); }
在组件中使用
'use client'; import { useState, useEffect } from 'react'; import { createTask, updateTask, deleteTask } from '../lib/actions/taskActions'; import { obtenerTasks } from '../lib/task/api_task'; export default function Todos({ attribute, value, title, limit, addBtn }: arguments) { const [tasks, setTasks] = useState([]); useEffect(() => { const fetchData = async () => { const res = await obtenerTasks({ attribute, value, limit }); setTasks(res.response); }; fetchData(); }, []); const handleUpdate = async (taskId, updatedData) => { await updateTask(taskId, updatedData); // 手动更新本地状态 setTasks(tasks.map(t => t.id === taskId ? { ...t, ...updatedData } : t)); }; return ( // 组件内容同前 ); }
内容的提问来源于stack exchange,提问作者own_appointment
相关产品推荐
相关产品推荐

