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

React Router中如何判断Root Layout子组件加载状态并实现导航栏加载提示?

解决React Router useNavigation状态始终为idle的问题

问题本质

useNavigation的state仅在路由处于加载阶段时才会更新,触发场景包括:

  • 使用lazy异步加载路由组件
  • 路由配置了loader函数且正在加载数据

你当前的代码中,所有页面组件都是同步导入的,路由切换瞬间完成,没有加载阶段,因此navigation.state始终保持idle。

解决方案

1. 针对路由切换(组件加载)显示状态

将页面组件改为懒加载,让路由切换产生加载阶段:

修改App.tsx,用React.lazy包装组件:

import { lazy, Suspense } from 'react';
import RootLayout from "@pages/Root"
import { RouterProvider, createBrowserRouter } from "react-router-dom"

// 懒加载页面组件
const DetailsPage = lazy(() => import("@pages/Details"));
const EpisodePage = lazy(() => import("@pages/Episode"));
const HomePage = lazy(() => import("@pages/Home"));

const router = createBrowserRouter([
  {
    path: '/',
    element: (
      <Suspense fallback={null}> {/* 不需要全局fallback,RootLayout自己控制加载状态 */}
        <RootLayout />
      </Suspense>
    ),
    children: [
      { index: true, element: <HomePage /> },
      { path: '/podcast/:podcastId', element: <DetailsPage /> },
      { path: '/podcast/:podcastId/episode/:episodeId', element: <EpisodePage /> }
    ]
  }
])

const App = () => {
  return <RouterProvider router={router} />
}

export default App

在RootLayout中判断加载状态并显示圆点:

import Navbar from '@components/UI/Navbar'
import { Outlet, useNavigation } from 'react-router-dom'

export const RootLayout = () => {
  const navigation = useNavigation()
  const isRouteLoading = navigation.state === "loading"

  return <>
          <Navbar>
            <h1>Podcasters</h1>
            {/* 加载圆点样式可自行调整 */}
            {isRouteLoading && <span style={{marginLeft:8, fontSize:18, color:'#666'}}>●</span>}
          </Navbar>
          <Outlet />
        </>
}

export default RootLayout

2. 针对页面数据加载显示状态

如果是页面内部加载数据(比如详情页获取接口数据),需要额外维护加载状态,可通过React Context传递到导航栏:

第一步:创建LoadingContext

// contexts/LoadingContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

type LoadingContextType = {
  isDataLoading: boolean;
  setIsDataLoading: (value: boolean) => void;
};

const LoadingContext = createContext<LoadingContextType>({
  isDataLoading: false,
  setIsDataLoading: () => {},
});

export const LoadingProvider = ({ children }: { children: ReactNode }) => {
  const [isDataLoading, setIsDataLoading] = useState(false);
  return (
    <LoadingContext.Provider value={{ isDataLoading, setIsDataLoading }}>
      {children}
    </LoadingContext.Provider>
  );
};

export const useLoading = () => useContext(LoadingContext);

第二步:在App中包裹Provider

// App.tsx
// ...其他导入
import { LoadingProvider } from './contexts/LoadingContext';

const App = () => {
  return (
    <LoadingProvider>
      <RouterProvider router={router} />
    </LoadingProvider>
  );
};

第三步:在RootLayout中合并状态

import { useLoading } from '../contexts/LoadingContext';

export const RootLayout = () => {
  const navigation = useNavigation();
  const { isDataLoading } = useLoading();
  
  // 合并路由加载和数据加载状态
  const showLoadingDot = navigation.state === "loading" || isDataLoading;

  return <>
          <Navbar>
            <h1>Podcasters</h1>
            {showLoadingDot && <span style={{marginLeft:8, fontSize:18, color:'#666'}}>●</span>}
          </Navbar>
          <Outlet />
        </>
}

第四步:在页面组件中更新状态

以DetailsPage为例:

import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useLoading } from '../contexts/LoadingContext';

const DetailsPage = () => {
  const { podcastId } = useParams();
  const { setIsDataLoading } = useLoading();
  const [data, setData] = useState<null | PodcastType>(null);

  useEffect(() => {
    const fetchData = async () => {
      setIsDataLoading(true);
      try {
        const res = await fetch(`/api/podcast/${podcastId}`);
        const result = await res.json();
        setData(result);
      } catch (err) {
        console.error('数据加载失败:', err);
      } finally {
        setIsDataLoading(false);
      }
    };

    fetchData();
  }, [podcastId, setIsDataLoading]);

  return data ? <div>{data.title}</div> : <div>内容加载中...</div>;
};

export default DetailsPage;

3. 使用React Router Loader函数(推荐)

给路由配置loader函数,在路由切换时预加载数据,此时useNavigation会自动进入loading状态:

修改App.tsx的路由配置:

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <HomePage /> },
      { 
        path: '/podcast/:podcastId', 
        element: <DetailsPage />,
        // 定义路由loader
        loader: async ({ params }) => {
          const res = await fetch(`/api/podcast/${params.podcastId}`);
          if (!res.ok) throw new Error('数据加载失败');
          return res.json();
        }
      },
      { 
        path: '/podcast/:podcastId/episode/:episodeId', 
        element: <EpisodePage />,
        loader: async ({ params }) => {
          const res = await fetch(`/api/podcast/${params.podcastId}/episode/${params.episodeId}`);
          if (!res.ok) throw new Error('数据加载失败');
          return res.json();
        }
      }
    ]
  }
])

在页面中用useLoaderData获取数据:

// DetailsPage.tsx
import { useLoaderData } from 'react-router-dom';

const DetailsPage = () => {
  const data = useLoaderData() as PodcastType;
  return <div>{data.title}</div>;
};

export default DetailsPage;

此时路由切换时,navigation.state会自动变为loading,RootLayout中直接用该状态即可显示加载圆点。

总结

  • 同步路由切换不会触发useNavigation的loading状态,需通过懒加载组件或loader函数制造加载阶段
  • 页面内部数据加载需自行维护状态,通过Context可传递到导航栏统一展示
  • 结合路由加载和数据加载状态,能实现完整的全局加载反馈

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 14:25:00