利用 React Router Outlet 实现组件嵌套渲染与布局管理

本文详细讲解如何在 react 应用中,利用 `react-router-dom` 的 `outlet` 组件和嵌套路由功能,实现将子组件渲染到父组件特定区域(如仪表盘的 `main-content`)。通过配置父路由作为布局容器,子路由作为内容视图,高效构建结构清晰、可维护的复杂页面布局,避免冗余的条件渲染。

在构建复杂的单页应用(SPA)时,尤其是在开发管理后台或仪表盘这类拥有固定布局(如侧边栏、顶部导航、主体内容区)的场景中,我们经常需要将不同的内容组件动态地渲染到布局中的特定区域。传统的做法可能涉及大量的条件渲染或通过 props 传递组件,但这往往会导致代码冗余、结构混乱且难以维护。react-router-dom 提供的 Outlet 组件结合嵌套路由功能,为这一挑战提供了优雅且专业的解决方案。

核心概念:嵌套路由与 Outlet

react-router-dom v6 引入了声明式嵌套路由的概念,允许你在一个父路由下定义子路由。当父路由匹配时,其对应的组件会被渲染,并且该组件内部可以放置一个 Outlet 组件。Outlet 的作用是作为子路由组件的占位符,当子路由匹配时,其对应的元素就会渲染在 Outlet 所在的位置。

这种机制的优势在于:

  1. 布局与内容的解耦:父组件(如 Dashboard)专注于提供布局结构(侧边栏、头部、主内容区域),而子组件(如 AddProduct、AdminMain)则专注于展示具体内容。
  2. 代码复用:布局组件只需渲染一次,所有子路由的内容都会在其内部的 Outlet 中切换,避免了重复的布局代码。
  3. 清晰的路由结构:路由配置更加直观,层级关系明确。

实现步骤

我们将以一个管理后台为例,演示如何将 AddProduct 组件渲染到 Dashboard 组件的 .main-content div 中。

步骤一:修改父布局组件 (Dashboard.js)

Dashboard 组件将作为管理后台的整体布局容器,包含侧边栏 (AdminSidebar)、头部 (AdminHeader) 和一个用于显示具体内容的主体区域。我们需要在这个主体区域内放置 Outlet。

原始 Dashboard.js 结构:

import React,{useState} from 'react'
import { Outlet } from 'react-router-dom'; // 已经引入,但未在正确位置使用
import AdminSidebar from '../AdminSidebar/AdminSidebar'
import AdminHeader from '../AdminHeader/AdminHeader';
import "./Dashboard.css"

function Dashboard() {
  const [checkboxChecked, setCheckboxChecked] = useState(false);
  // ... 其他逻辑 ...

  return (
    <>
      
      
        
      

      
      
    
  )
}

export default Dashboard

修改 Dashboard.js:

在 main-content div 内部,AdminHeader 组件之后,添加 。这样,所有嵌套的子路由组件都将在此处渲染。

import React,{useState} from 'react'
import { Outlet } from 'react-router-dom';
import AdminSidebar from '../AdminSidebar/AdminSidebar'
import AdminHeader from '../AdminHeader/AdminHeader';
import "./Dashboard.css"

function Dashboard() {
  const [checkboxChecked, setCheckboxChecked] = useState(false);
  const handleCheckboxChange = (event) => {
    console.log("working")
    const sidebar = document.querySelector(".sidebar");
    const mainContent = document.querySelector(".main-content");

    if (sidebar && mainContent) {
      sidebar.style.left = event.target.checked ? "-100%" : "0";
      mainContent.style.marginLeft = event.target.checked ? "0" : "";
      const mainContentHeader = mainContent.querySelector("header");
      if (mainContentHeader) {
        mainContentHeader.style.left = event.target.checked ? "0" : "";
        mainContentHeader.style.width = event.target.checked ? "100%" : "";
        mainContentHeader.style.right = event.target.checked ? "0" : "";
      }
    }
  };

  const handleToggleClick = () => {
    setCheckboxChecked(!checkboxChecked);
    handleCheckboxChange({ target: { checked: !checkboxChecked } });
  };


  return (
    <>
      
      
        
         {/* 在此渲染嵌套路由的组件 */}
      

      
      
    
  )
}

export default Dashboard

步骤二:配置 App.js 中的嵌套路由

在 App.js 中,我们需要将 Dashboard 组件定义为一个父路由的元素,并将 AdminMain 和 AddProduct 定义为它的子路由。

原始 App.js 路由配置:

// ... 其他导入和状态管理 ...

return (
  <>
    
      {adminRoute ?   : 
} }/> }/> );

在原始配置中,Dashboard 组件是根据 adminRoute 状态进行条件渲染的,并且 AdminMain 和 AddProduct 是独立的顶级路由。这种方式无法实现 AddProduct 在 Dashboard 内部特定区域的渲染。

修改 App.js 路由配置:

我们将创建一个父 Route,其 path 为 /admin/* 并渲染 Dashboard 组件。然后,将 AdminMain 和 AddProduct 作为其子 Route。注意,子路由的 path 是相对于父路由的,因此只需指定相对路径。

import { useState,useEffect } from 'react';
import './App.css';
import Header from './Components/Header/Header';
import { BrowserRouter as Router, Route ,Routes} from 'react-router-dom';
import Pages from './Pages/Pages';
import Data from "./Components/FlashDeals/Data"
import Cart from './Components/Cart/Cart';
import Sdata from './Components/Shop/Sdata'; 
import {auth} from "../src/Firebase/Firebase"
import Dashboard from './Admin/Dashboard/Dashboard';
import AdminMain from './Admin/AdminMain/AdminMain';
import AddProduct from './Admin/AddProduct/AddProduct';


function App() {
  const  productItems = Data.productItems
  const {shopItems} = Sdata
  const [cartItem,setCartItem] = useState([]);
  const [userData,setUserData] = useState(undefined);
  const [adminRoute,setAdminRoute] = useState(false)

  console.log(adminRoute)

  useEffect(()=>{
    if(window.location.pathname.startsWith('/admin')){
      setAdminRoute(true)
    }else{
      setAdminRoute(false)
    }
  },[])





  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      if (user && user.emailVerified) {
        console.log("user is",user)
        setUserData(user)
      }
    });

    return () => unsubscribe();
  }, []);

  const handleSignOut = () => {
    auth.signOut()
      .then(() => {
        setUserData(undefined);
        setCartItem([]);
      })
      .catch((error) => {
        console.log('Sign out error:', error);
      });
  };


  const addToCart = (product)=>{
    const productExit = cartItem.find((item)=>item.id === product.id)
    if(productExit){
      setCartItem(cartItem.map((item)=>
        (item.id === product.id ? {...productExit,qty:productExit.qty + 1} : item)
      ))
    }else{
      setCartItem([...cartItem,{...product,qty:1}])
    }
  }

  const decreaseQty = (product) =>{
    const productExit = cartItem.find((item)=>item.id === product.id)
    if(productExit.qty === 1){
      setCartItem(cartItem.filter((item)=>item.id !== product.id))
    }else{
      setCartItem(cartItem.map((item)=>(item.id=== product.id ? {...productExit,qty : productExit.qty-1}:item)))
    }
  }

  return (
    <>
      
        {/* 根据 adminRoute 状态决定渲染 Dashboard 还是 Header,
            但 Dashboard 内部的路由现在由嵌套路由管理 */}
        {adminRoute ?   : 
} }/> }/> {/* 定义 /admin 的父路由,渲染 Dashboard 作为布局组件 */} }> {/* 子路由,路径是相对于父路由的 */} {/* 也可以添加一个索引路由,当访问 /admin 时渲染默认内容 */} {/* */} ); } export default App;

说明: