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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 03:43:44