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

React Router如何流式加载非关键数据,避免UI整体等待?

问题描述

我有一个CSR应用,路由定义如下:

const router = createBrowserRouter([
  {
    path: '/app/',
    element: <NavBar />,
    loader: navBarLoader,
    children: [
      {
        path: 'placement-selection',
        Component: PlacementSelection,
        loader: placementSelectionLoader,
        children: [
          {
            path: 'options',
            Component: PlacementSelectionOptions,
            loader: placementSelectionOptionsLoader,
            children: [
              {
                path: 'summary',
                Component: PlacementSelectionOptionsSummary,
                loader: placementSelectionOptionsSummaryLoader,
              },
            ],
          },
        ],
      },
    ],
  },
]);

对应的loader实现:

export async function navBarLoader() {
  console.log('called navBarLoader')
  return await new Promise((res, rej) => {
    setTimeout(() => res('response'), 2000)
  })
}

export async function placementSelectionLoader() {
  console.log('called placementSelectionLoader')
  return await new Promise((res, rej) => {
    setTimeout(() => res('response'), 2000)
   })
}

export async function placementSelectionOptionsLoader() {
  console.log('called placementSelectionOptionsLoader')
  return await new Promise((res, rej) => {
    setTimeout(() => res('response'), 2000)
  })
}

export async function placementSelectionOptionsSummaryLoader() {
  console.log('called placementSelectionOptionsSummaryLoader')
  return new Promise((res, rej) => {
    setTimeout(() => res('response'), 10000)
  })
}

PlacementSelectionOptionsSummary组件使用loader数据的方式:

import { Suspense } from 'react';
import './styles.css';
import { Await, useLoaderData } from 'react-router';

export default function PlacementSelectionOptionsSummary() {
  const loaderData = useLoaderData();
  return (
    <>
      <Suspense fallback={<div>Loading...</div>}>
        <Await resolve={loaderData}>
          {value => <h3>Non critical value: {value}</h3>}
        </Await>
      </Suspense>
      <div className="main-container" style={{ width: '30%' }}>
        <div className="container" style={{ height: '500px' }}>
          <div className="card" style={{ backgroundColor: 'blue' }}>
            <span className="description">
              {' '}
              'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean
              tempor ante ligula, id aliquet ligula laoreet id. In aliquam
              hendrerit mauris, at sagittis tortor efficitur non. Proin egestas
              facilisis augue. Sed aliquet maximus tempus. Nullam ornare
              efficitur blandit. Sed eu mattis tortor. Lorem ipsum dolor sit
              amet, consectetur adipiscing elit. Aenean tempor ante ligula, id
              aliquet ligula laoreet id. In aliquam hendrerit mauris, at
              sagittis tortor efficitur non. Proin egestas facilisis augue. Sed
              aliquet maximus tempus. Nullam ornare efficitur blandit. Sed eu
              mattis tortor',
            </span>
          </div>
        </div>
      </div>
    </>
  );
}

可以看到,placementSelectionOptionsSummaryLoader加载耗时10秒,其余loader均为2秒。按照React Router文档说明,直接返回Promise就能实现流式加载,但访问/app/placement-selection/options/summary时,UI仍要等所有loader完成才渲染。想实现大部分UI优先渲染,仅PlacementSelectionOptionsSummary组件在慢loader完成前显示加载占位符。


解决方案

核心问题在于父路由的loader会阻塞子路由的渲染——React Router会先等待所有上层路由的loader完成后,才会开始渲染子路由组件,包括其中的Suspense部分。结合你的场景,可通过以下方式调整:

1. 修正路由配置的组件属性

你的路由配置中误用了Component(大写C),React Router v6要求路由组件使用element(小写e)属性。这个错误可能导致路由渲染逻辑异常,是阻塞问题的潜在诱因。修正后的路由:

const router = createBrowserRouter([
  {
    path: '/app/',
    element: <NavBar />,
    loader: navBarLoader,
    children: [
      {
        path: 'placement-selection',
        element: <PlacementSelection />, // 替换Component为element
        loader: placementSelectionLoader,
        children: [
          {
            path: 'options',
            element: <PlacementSelectionOptions />, // 替换Component为element
            loader: placementSelectionOptionsLoader,
            children: [
              {
                path: 'summary',
                element: <PlacementSelectionOptionsSummary />, // 替换Component为element
                loader: placementSelectionOptionsSummaryLoader,
              },
            ],
          },
        ],
      },
    ],
  },
]);

2. 使用React Router的defer API实现流式渲染

defer可以让loader返回延迟加载的Promise,配合Await组件实现局部加载,不阻塞页面其他部分的渲染。

步骤1:修改慢loader使用defer

import { defer } from 'react-router';

export async function placementSelectionOptionsSummaryLoader() {
  console.log('called placementSelectionOptionsSummaryLoader');
  // 将慢请求包装为Promise,通过defer返回
  const slowDataPromise = new Promise((res) => {
    setTimeout(() => res('response'), 10000);
  });
  return defer({ slowData: slowDataPromise });
}

步骤2:组件中获取延迟数据

import { Suspense } from 'react';
import './styles.css';
import { Await, useLoaderData } from 'react-router';

export default function PlacementSelectionOptionsSummary() {
  const { slowData } = useLoaderData();
  return (
    <>
      <Suspense fallback={<div>Loading...</div>}>
        <Await resolve={slowData}>
          {value => <h3>Non critical value: {value}</h3>}
        </Await>
      </Suspense>
      {/* 这部分UI会优先渲染,无需等待慢loader */}
      <div className="main-container" style={{ width: '30%' }}>
        <div className="container" style={{ height: '500px' }}>
          <div className="card" style={{ backgroundColor: 'blue' }}>
            <span className="description">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean
              tempor ante ligula, id aliquet ligula laoreet id. In aliquam
              hendrerit mauris, at sagittis tortor efficitur non. Proin egestas
              facilisis augue. Sed aliquet maximus tempus. Nullam ornare
              efficitur blandit. Sed eu mattis tortor. Lorem ipsum dolor sit
              amet, consectetur adipiscing elit. Aenean tempor ante ligula, id
              aliquet ligula laoreet id. In aliquam hendrerit mauris, at
              sagittis tortor efficitur non. Proin egestas facilisis augue. Sed
              aliquet maximus tempus. Nullam ornare efficitur blandit. Sed eu
              mattis tortor
            </span>
          </div>
        </div>
      </div>
    </>
  );
}

效果说明

  • 父路由的2秒loader完成后,页面基础结构(NavBar、PlacementSelection、PlacementSelectionOptions)会立即渲染。
  • PlacementSelectionOptionsSummary中的蓝色卡片部分会优先显示,仅依赖慢数据的<h3>区域会显示加载占位符,直到10秒后更新。

3. 进一步优化:拆分父路由非必要loader逻辑

如果希望父组件也不被2秒的loader阻塞,可以将父路由中不需要全局共享的请求,移到组件内部通过useEffect异步获取,避免整个路由层级的渲染阻塞。


内容的提问来源于stack exchange,提问作者Gambit2007

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 15:15:55