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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 05:51:00