Next.js + TypeScript 仪表板数据获取优化重构咨询
问题:Next.js Dashboard页面多API并行请求加载缓慢,求重构与优化方案
我是Next.js和TypeScript的新手,当前我的dashboard.tsx页面通过Promise.all并行请求多个API接口,导致页面加载耗时过长,希望对代码进行重构以提升数据获取速度。同时希望得到编码模式的优化建议,后端使用Laravel框架。我曾尝试过React的useMemo,但不确定是否适用于当前需求。
相关代码
dashboard.tsx
import Layout from "@/layouts/layout"; import React, { useContext, useEffect, useState } from "react"; import { LanguageContext } from "@/hooks/Lang/LanguageContext"; import Card from "@/components/Cards/dashboardCard"; import Link from "next/link"; import { editData, getMultipleData, getSingleData, } from "@/lib/ApiRequestHelper"; import DepositWithdrawTable from "@/components/Table/DepositWithdrawTable"; import PerformanceTable from "@/components/Table/PerformanceTable"; import { checkUserRoles } from "@/lib/checkUserRole"; import Spinner from "@/components/Spinner/Spinner"; import GeneralOutcomeTable from "@/components/Table/GeneralOutcomeTable"; type ReportData = { data: { amount: number; confirmStatus: number; id: number; reporter: { id: number; name: string; }; type: string; [key: string]: any; // 兼容额外属性 }; }; const Dashboard = () => { const { t } = useContext(LanguageContext); const [income, setIncome] = useState<number>(0); const [outcome, setOutcome] = useState<number>(0); const [regularData, setRegularData] = useState<ReportData[]>([]); const [balanceData, setBalanceData] = useState<any[]>([]); const [userCount, setUserCount] = useState(0); const [reportsCount, setReportsCount] = useState(0); const [loading, setLoading] = useState<boolean>(true); const [userRole, setUserRole] = useState<string>(""); const [data, setData] = useState<ReportData[]>([]); // 初始化为空数组 const fetchData = async () => { const [fetchFinancial, fetchUsers, fetchReports, fetchUncheckReports, fetchRegularData, role] = await Promise.all([ getMultipleData("/calculations"), getMultipleData("/users"), getMultipleData("/reports"), getMultipleData("/uncheck-reports"), getMultipleData('/general-outcome'), checkUserRoles() ]); try { setUserCount(fetchUsers.data.length); setReportsCount(fetchReports.meta.totalItems); setIncome(fetchFinancial.data.income); setOutcome(fetchFinancial.data.outcome); setBalanceData(fetchFinancial.data); setData(fetchUncheckReports.data); setRegularData(fetchRegularData.data); setUserRole(role); setLoading(false); } catch (error) { setLoading(false); } }; const handleAccept = async (id: number) => { await editData(`/reports/${id}/accept`, ""); fetchData(); }; const handleReject = async (id: number) => { await getSingleData(`/reports/${id}/reject`); fetchData(); }; useEffect(() => { fetchData(); }, []); return ( <Layout> <div> {loading ? ( <Spinner /> ) : ( <div className=""> <nav aria-label="breadcrumb" className="w-full p-4 dark:bg-gray-800 dark:text-gray-100 pl-8"> <ol className="flex h-8 space-x-2 dark:text-gray-100"> <li className="flex items-center"> <Link rel="noopener noreferrer" href="/app/home" title="返回首页" className="flex items-center hover:underline"><i className="fa-solid fa-house" /></Link> </li> <li className="flex items-center space-x-1"> <span className="dark:text-gray-400">/</span> <Link rel="noopener noreferrer" href="/app/dashboard" className="flex items-center px-1 capitalize hover:underline">仪表盘</Link> </li> </ol> </nav> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3 p-8"> <div className="w-full sm:w-full md:w-1/4 p-2"> <Link href="/app/reports"> <Card title={t("income")} data={income} /> </Link> </div> <div className="w-full sm:w-full md:w-1/4 p-2"> <Link href="/app/reports"> <Card title={t("outcome")} data={outcome} /> </Link> </div> <div className="w-full sm:w-full md:w-1/4 p-2"> <Link href="/app/reports"> <Card title={t("reports")} data={reportsCount} /> </Link> </div> <div className="w-full sm:w-full md:w-1/4 p-2"> <Link href="/app/users"> <Card title={t("users")} data={userCount} /> </Link> </div> </div> <div className="grid grid-cols-1 md:grid-cols-2 gap-5 p-8"> <div className="w-full"> <DepositWithdrawTable onAccept={handleAccept} onReject={handleReject} data={data} userRole={userRole} /> </div> <div className="w-full"> <PerformanceTable balanceData={balanceData} /> </div> </div> <div className="grid grid-cols-1 md:grid-cols-2 gap-5 p-8"> <div className="w-full"> <GeneralOutcomeTable data={regularData} userRole={userRole} /> </div> </div> </div> )} </div> </Layout> ); }; export default Dashboard;
getMultipleData函数
import { callApi } from '@/hooks/callApi' const defaultRoute = '/api/app'; export async function getMultipleData(url: string) { return await callApi('get', defaultRoute + url) }
解决方案与优化建议
一、API层面优化(Laravel后端)
最有效的提速方式是聚合API请求,避免前端发起6个独立请求:
- 在Laravel后端新增一个
dashboard接口,一次性返回所有仪表盘需要的数据:用户总数、报表总数、收支数据、待审核报表、常规支出数据、用户角色等。 - 示例后端路由与控制器逻辑:
// routes/api.php Route::get('/app/dashboard', [DashboardController::class, 'index']); // app/Http/Controllers/DashboardController.php public function index(Request $request) { $user = $request->user(); $userCount = User::count(); $reportsCount = Report::count(); $financialData = Calculation::first(['income', 'outcome']); $uncheckReports = Report::where('confirmStatus', 0)->get(); $regularOutcome = GeneralOutcome::all(); $userRole = $user->role; return response()->json([ 'user_count' => $userCount, 'reports_count' => $reportsCount, 'financial' => $financialData, 'uncheck_reports' => $uncheckReports, 'regular_outcome' => $regularOutcome, 'user_role' => $userRole ]); }
这样前端只需发起1次请求,减少网络握手开销,大幅提升加载速度。
二、前端请求与状态优化
1. 重构请求逻辑
将多个getMultipleData调用替换为单个请求:
// 新增getDashboardData函数 export async function getDashboardData() { return await callApi('get', defaultRoute + '/dashboard'); } // Dashboard组件中的fetchData修改为: const fetchData = async () => { try { const response = await getDashboardData(); const { user_count, reports_count, financial, uncheck_reports, regular_outcome, user_role } = response.data; setUserCount(user_count); setReportsCount(reports_count); setIncome(financial.income); setOutcome(financial.outcome); setBalanceData(financial); setData(uncheck_reports); setRegularData(regular_outcome); setUserRole(user_role); setLoading(false); } catch (error) { setLoading(false); // 可添加错误提示逻辑 } };
2. 避免全量刷新
当前handleAccept/handleReject操作后会重新请求所有数据,可优化为局部更新状态:
const handleAccept = async (id: number) => { await editData(`/reports/${id}/accept`, ""); // 直接从data中移除已审核的项,避免全量请求 setData(prev => prev.filter(item => item.data.id !== id)); // 如果需要更新报表总数,也可以直接减1 setReportsCount(prev => prev - 1); }; const handleReject = async (id: number) => { await getSingleData(`/reports/${id}/reject`); setData(prev => prev.filter(item => item.data.id !== id)); setReportsCount(prev => prev - 1); };
3. useMemo的正确使用场景
useMemo适合缓存计算开销大的结果,比如对表格数据的排序、过滤,而非缓存API请求结果。如果你的表格需要频繁处理数据,可以用useMemo缓存处理后的数据:
import { useMemo } from 'react'; // 示例:缓存处理后的待审核报表数据 const processedData = useMemo(() => { return data.map(item => ({ ...item.data, formattedAmount: `$${item.data.amount.toFixed(2)}` })); }, [data]); // 传给组件时用processedData <DepositWithdrawTable onAccept={handleAccept} onReject={handleReject} data={processedData} userRole={userRole} />
三、编码模式优化建议
- 状态聚合:将分散的状态(income、outcome、userCount等)合并为一个对象状态,减少重复的useState:
const [dashboardState, setDashboardState] = useState({ income: 0, outcome: 0, userCount: 0, reportsCount: 0, balanceData: [], data: [], regularData: [], userRole: "" }); // 更新时: setDashboardState(prev => ({ ...prev, user_count: response.data.user_count, // 其他字段... }));
- 自定义Hook封装数据请求:将仪表盘数据请求逻辑抽离为自定义Hook,提高复用性和可读性:
// hooks/useDashboardData.ts import { useState, useEffect } from 'react'; import { getDashboardData } from '@/lib/ApiRequestHelper'; export const useDashboardData = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { const fetch = async () => { try { const response = await getDashboardData(); setData(response.data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetch(); }, []); return { loading, data, error }; }; // 在Dashboard组件中使用: const { loading, data, error } = useDashboardData(); // 解构data中的字段使用
- 类型安全优化:替换
any类型为明确的TypeScript接口,避免类型错误:
// 定义仪表盘数据接口 interface DashboardData { user_count: number; reports_count: number; financial: { income: number; outcome: number; [key: string]: any; }; uncheck_reports: ReportData[]; regular_outcome: ReportData[]; user_role: string; } // 状态定义时使用该接口 const [dashboardState, setDashboardState] = useState<DashboardData>({ // 初始值... });
- 错误处理增强:添加错误提示UI,让用户知道请求失败,而不是只隐藏加载状态:
{loading ? ( <Spinner /> ) : error ? ( <div className="p-8 text-red-500">数据加载失败,请稍后重试</div> ) : ( // 正常渲染内容 )}
内容的提问来源于stack exchange,提问作者LwinMoeAung
相关产品推荐
相关产品推荐

