Search code examples
javascriptreactjsreact-routerreact-router-dom

React loader doesn't work with descendent routes


I have a App.js file that includes all the routes that I have. I wanted to make use of React-Router data loader.

import React from 'react'
import { Routes, Route, Navigate, RouterProvider, createBrowserRouter, createRoutesFromElements} from 'react-router-dom'
import { ThemeProvider, CssBaseline } from '@mui/material'
import Login from './pages/global/Login'
import NoSidebarLayout from './pages/global/NoSidebarLayout'
import SidebarLayout from './pages/global/SidebarLayout'
import NotFound from './pages/NotFound'
import ComingSoon from './pages/ComingSoon'
import Home from './pages/Home'
import About from './pages/About'
import Contact from './pages/Contact'
import ItemRoutes from './pages/ItemRoutes'
import CountryRoutes from './pages/country/CountryRoutes'
import PortRoutes from './pages/port/PortRoutes';
import ServicesManagerRoutes from './pages/serviceManager/ServicesManagerRoutes';
import ChargeManagerRoutes from  './pages/chargeManager/ChargeManagerRoutes';
import PortChargesRoutes from './pages/portCharges/PortChargesRoutes';
import PortsRoutes from './pages/ports/PortsRoutes';
import PrivateRoutes from './components/PrivateRoutes';
import { ColorModeContext, useAppTheme } from './theme'
import { Provider } from "react-redux";
import store from "./redux/store";
import CountryList, { loader as CountryListLoader } from './pages/country/CountryList';
import Country from './pages/country/Country'

const router = createBrowserRouter(createRoutesFromElements(
  <Route>
    <Route element={<PrivateRoutes />}>
      <Route element={<SidebarLayout />}>
        <Route path="/ui/" element={<Home />} />
        <Route path="/ui/items/*" element={<ItemRoutes />} />
        <Route path="/ui/countries/*" element={<CountryRoutes />} />
        <Route path="/ui/ports/*" element={<PortRoutes />} />
        <Route path="/ui/contact" element={<Contact />} />
        <Route path="/ui/about" element={<About />} />
        <Route path="/ui/service-manager/*" element={<ServicesManagerRoutes />} />
        <Route path="/ui/charge-manager/*" element={<ChargeManagerRoutes />} />
        <Route path="/ui/port-charges/*" element={<PortChargesRoutes />} />
        <Route path="/ui/port-manager/*" element={<PortsRoutes />} />
        <Route path="/ui/coming-soon/*" element={<ComingSoon />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Route>
    <Route element={<NoSidebarLayout />}>
      <Route path="/ui/login" element={<Login />} />
      <Route path="*" element={<NotFound />} />
    </Route>
  </Route>
))

const App = () => {
  const [theme, colorMode] = useAppTheme();

  return (
    <Provider store={store}>
      <ColorModeContext.Provider value={colorMode}>
        <ThemeProvider theme={theme}>
          <CssBaseline />
          <div className="app">
            <RouterProvider router={router}/>
          </div>
        </ThemeProvider>
      </ColorModeContext.Provider>
    </Provider>
  )
}

export default App

on the CountryRoutes I have this, which includes the loader

import React from 'react'
// import { Routes, Route } from 'react-router-dom';
import { Routes, Route, Navigate, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom'
import CountryList, { loader as CountryListLoader } from './CountryList';
import Country from './Country';

const CountryRoutes = () => (
  <Routes>
    <Route >
      <Route index element={<CountryList />} loader={CountryListLoader} />
      <Route path=":id" element={<Country />} />
    </Route>
  </Routes>
);

export default CountryRoutes;

With this setup the loader is not working but when I directly add the children routes to the parent like this,

const router = createBrowserRouter(createRoutesFromElements(
  <Route>
    <Route element={<PrivateRoutes />}>
      <Route element={<SidebarLayout />}>
        <Route path="/ui/" element={<Home />} />
        <Route path="/ui/items/*" element={<ItemRoutes />} />
        <Route path="/ui/countries/*">
          {/* Directly adding the children routes  */}
          <Route index element={<CountryList />} loader={CountryListLoader} />
          <Route path=":id" element={<Country />} />
        </Route>
        <Route path="/ui/ports/*" element={<PortRoutes />} />
        <Route path="/ui/contact" element={<Contact />} />
        <Route path="/ui/about" element={<About />} />
        <Route path="/ui/service-manager/*" element={<ServicesManagerRoutes />} />
        <Route path="/ui/charge-manager/*" element={<ChargeManagerRoutes />} />
        <Route path="/ui/port-charges/*" element={<PortChargesRoutes />} />
        <Route path="/ui/port-manager/*" element={<PortsRoutes />} />
        <Route path="/ui/coming-soon/*" element={<ComingSoon />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Route>
    <Route element={<NoSidebarLayout />}>
      <Route path="/ui/login" element={<Login />} />
      <Route path="*" element={<NotFound />} />
    </Route>
  </Route>
))

Notice that I directly added the children route to the "/ui/countries/*". I am a bit confused to which router should I add the loader.


Solution

  • This bug is most likely being caused by the use of the <Routes/> component.

    Per the docs, <Routes/> is seldom used in conjunction with a data router such as createBrowserRouter(). Why? Because of this well-hidden tidbit of knowledge, which can be paraphrased thus—

    Unlike in BrowserRouter, if a <Routes/> component is used in the new RouterProvider, its children cannot leverage any of the new Data APIs.

    Okay, so how can you ensure all routes can access the data APIs? You have two options.

    1. You can lift each <Route> defined within a <Routes/> component to the top level, Continue shifting them upward one by one until all routes are defined as direct children of the RouterProvider. In your case this is createBrowserRouter().

    Although unaware, you did this by directly adding the children's routes to the parent; this also explains why the loader works.

    1. You can use a <Route/> component to create a pathless layout route around the target routes instead of using <Routes/>. However, for this to work, an <Outlet/> must be added for the child routes to render, especially the index. Index routes, as defined in the docs, render "in their parent route's outlet at the parent route's path."

    In your case, you could edit the <CountryRoutes /> component to look something like this:

    const CountryRoutes = () => (
    
      <>
        <h1>Countries</h1>
        <Outlet />
      </>
    
    )
    

    Then, in the <App /> component, you could nest the index and dynamic country routes.

    <Route element={<SidebarLayout />}>
    
      <Route path='/ui/' element={<Home />} />
      <Route path='/ui/items/*' element={<ItemRoutes />} />
    
      <Route path='/ui/countries/*' element={<CountryRoutes />}>
        <Route index element={<CountryList />} loader={CountryListLoader}/>
        <Route path=':id' element={<Country />} />
      </Route>
    
      <Route path='/ui/ports/*' element={<PortRoutes />} />
      <Route path='/ui/contact' element={<Contact />} />
      <Route path='/ui/about' element={<About />} />
    
      ...
    </Route>
    

    Of course, although these edits should provide a good starting point, you will likely need to tweak them a bit for best results.