Remix中如何避免父子路由Loader重复获取相同API数据?
Remix父子路由共享API数据解决方案
问题背景
路由结构如下:
routes |-- teams.tsx |-- teams.$id.tsx root.tsx
业务需求:
/teams路由:获取所有团队数据,仅1条时重定向到对应详情页,多条则展示列表/teams/$id路由:根据ID匹配团队,无匹配则重定向到/teams并提示“未找到团队”,匹配成功展示详情
当前问题:父子路由Loader并行执行,导致相同API被请求两次;尝试用useMatch获取父路由数据时,与remix-toast冲突,出现Error: Objects are not valid as a React child (found: [object Promise])错误,且重定向逻辑只能在Loader中执行,无法通过组件Hook解决。
现有代码示例
teams.tsx
export const loader: LoaderFunction = async ({ request }) => { // session const session = await sessionStorage.getSession(request.headers.get('Cookie')); if (!session.get('token')) { return redirectWithToast(`/`, { message: "You don’t have access!", type: "error" } ) } // data const teams = await fetch(...args).then((res) => res.json()); if (teams.length === 1) { const teamsId = teams[0].id; return redirect(`/teams/${teamsId}`); } return { session, teams }; } export default function Teams() { const data = useLoaderData<typeof loader>(); console.log(data.session) console.log(data.teams) }
teams.$id.tsx
export const loader: LoaderFunction = async ({ request, params }) => { // session const session = await sessionStorage.getSession(request.headers.get('Cookie')); if (!session.get('token')) { return redirectWithToast(`/`, { message: "You don’t have access!", type: "error" } ) } // data: 和teams.tsx请求相同数据 const teams = await fetch(...args).then((res) => res.json()); const team = teams.find(t => t.id === params.id); if (!team) { return redirectWithToast(`/teams`, { message: "Team not found!", type: "error" } ) } return { session, teams, team }; } export default function TeamDetails() { const data = useLoaderData<typeof loader>(); console.log(data.session) console.log(data.team) }
解决方案:抽离共享逻辑+缓存复用
1. 抽离共享工具函数
把重复的session验证、数据请求逻辑抽成独立函数,避免代码冗余的同时实现数据复用:
// 可放在utils/teams.ts或单独文件中 import { sessionStorage } from "~/services/session.server"; import { redirectWithToast } from "~/utils/toast.server"; // 验证session并返回,无效则返回重定向响应 export async function getValidSession(request: Request) { const session = await sessionStorage.getSession(request.headers.get('Cookie')); if (!session.get('token')) { return redirectWithToast(`/`, { message: "你没有访问权限!", type: "error" }); } return session; } // 缓存团队数据,同一个请求周期内避免重复请求 const teamsCache = new Map<string, any[]>(); export async function getTeamsData() { if (teamsCache.has('teams')) { return teamsCache.get('teams')!; } const teams = await fetch(...args).then(res => res.json()); teamsCache.set('teams', teams); // 可选:添加缓存过期逻辑,防止内存泄漏 setTimeout(() => teamsCache.delete('teams'), 5000); return teams; }
2. 修改父子路由Loader
teams.tsx的Loader
import { getValidSession, getTeamsData } from "~/utils/teams"; export const loader: LoaderFunction = async ({ request }) => { const sessionResult = await getValidSession(request); // 如果session验证失败,直接返回重定向响应 if (sessionResult instanceof Response) { return sessionResult; } const teams = await getTeamsData(); if (teams.length === 1) { const teamId = teams[0].id; return redirect(`/teams/${teamId}`); } return { session: sessionResult, teams }; };
teams.$id.tsx的Loader
import { getValidSession, getTeamsData } from "~/utils/teams"; export const loader: LoaderFunction = async ({ request, params }) => { const sessionResult = await getValidSession(request); if (sessionResult instanceof Response) { return sessionResult; } const teams = await getTeamsData(); const team = teams.find(t => t.id === params.id); if (!team) { return redirectWithToast(`/teams`, { message: "未找到团队!", type: "error" }); } return { session: sessionResult, team, teams }; };
3. 优化缓存策略(可选)
如果是多服务器实例部署,内存缓存可能不一致,可改用以下方式:
- 在
fetch请求中添加Cache-Control头,让浏览器/CDN缓存数据:const teams = await fetch(...args, { headers: { 'Cache-Control': 'max-age=60' // 缓存60秒 } }).then(res => res.json()); - 在Loader返回结果时设置缓存头:
return json({ session: sessionResult, teams }, { headers: { 'Cache-Control': 'max-age=60' } });
关于useRouteLoaderData的说明
如果不想用缓存,也可以让子路由通过useRouteLoaderData获取父路由的数据,但要注意:父子路由Loader并行执行,子路由Loader无法直接依赖父路由的Loader结果,因此重定向逻辑仍需在Loader中处理,组件中仅用于展示数据。组件中使用方式:
import { useRouteLoaderData, useParams } from '@remix-run/react'; export default function TeamDetails() { const params = useParams(); const parentData = useRouteLoaderData('routes/teams') as { teams: any[] }; const team = parentData?.teams.find(t => t.id === params.id); return <div>团队名称:{team?.name}</div>; }
但这种方式无法在子路由Loader中做重定向逻辑(因为Loader并行,父路由可能还没返回数据),因此更推荐前面的抽离+缓存方案。
内容的提问来源于stack exchange,提问作者arlioli03
相关产品推荐
相关产品推荐

