skip to Main Content

I have prepared a simple test case at Github for my question:

animated screenshot

In RouterLayout.jsx a media query makes the Outlet disappear at narrow screens, as you can see in the animated screenshot, where I drag the browser side to make it smaller:

RootLayout.jsx

import { Outlet } from "react-router";
import { useMediaQuery } from "@react-hook/media-query";
import MasterList from "../components/MasterList";

export default function RootLayout() {
  const isSmallScreen = useMediaQuery("(max-width: 768px)");

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "space-around",
        alignItems: "flex-start",
      }}
    >
      <MasterList />
      {!isSmallScreen && (
        <div>
          <h3>Detail</h3>
          <Outlet />
        </div>
      )}
    </div>
  );
}

My question please:

At small mobile phone screens only the Master List should be displayed. And when a Link is clicked there, then a Page1 or other pages should be displayed, with a possibility to return back to the Master List. This does not work yet.

At large tablet and computer screens both Master List and Page1 or others are displayed near each other. This works in my code already.

Should I add some attribute to the Links in the MasterList.jsx below?

MasterList.jsx

import { Link } from "react-router-dom";

const MasterList = () => {
  const pages = [
    { id: 1, title: "Page 1" },
    { id: 2, title: "Page 2" },
    { id: 3, title: "Page 3" },
    { id: 4, title: "Page 4" },
  ];

  return (
    <div>
      <h2>Master List</h2>
      <nav>
        <ul>
          {pages.map((page) => (
            <li key={page.id}>
              <Link to={"/page" + page.id}>{page.title}</Link>
            </li>
          ))}
        </ul>
      </nav>
    </div>
  );
};

export default MasterList;

App.jsx

import {
  Route,
  RouterProvider,
  createBrowserRouter,
  createRoutesFromElements,
} from "react-router-dom";
import Page1 from "./pages/Page1";
import Page2 from "./pages/Page2";
import Page3 from "./pages/Page3";
import Page4 from "./pages/Page4";
import RootLayout from "./layouts/RootLayout";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<RootLayout />}>
      <Route path="page1" element={<Page1 />} />
      <Route path="page2" element={<Page2 />} />
      <Route path="page3" element={<Page3 />} />
      <Route path="*" element={<Page4 />} />
    </Route>
  )
);

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

export default App;

2

Answers


  1. I don’t know what the best approach to achieve this is, but it needs to incorporate the fact that Outlet renders child routes in the Parent Component.

    I would explore useMatches to see if you can use it to determine whether the Parent component was rendered on /parent or /parent/child. With that information, you could conditionally render the navigation elements:

    • If on /parent, always render navigation
    • If on /parent/child and screen is small, don’t render navigation
    • Always render Outlet
    Login or Signup to reply.
  2. On smaller view sizes where the Outlet isn’t rendered the nested Route components no longer have a place to render out their element content. If I understand your post correctly you want to use RootLayout and the Outlet on the larger non-"mobile" view sizes, and on mobile view sizes render RootLayout as a sibling route instead of a parent route, so all the other routes are accessible.

    Example refactor:

    • Create two root layouts, mobile and non-mobile.

      function RootLayout() {
        return (
          <div
            style={{
              display: "flex",
              justifyContent: "space-around",
              alignItems: "flex-start"
            }}
          >
            <MasterList />
            <div>
              <h3>Detail</h3>
              <Outlet />
            </div>
          </div>
        );
      }
      
      function MobileLayout() {
        return (
          <div
            style={{
              display: "flex",
              justifyContent: "space-around",
              alignItems: "flex-start"
            }}
          >
            <div>
              <h3>Detail</h3>
              <Outlet />
            </div>
          </div>
        );
      }
      
    • Update App to use the media query hook and conditionally render one root layout or the other, and conditionally render the MasterList as an index route on smaller screens.

      export default function App() {
        const isSmallScreen = useMediaQuery("(max-width: 768px)");
      
        const router = React.useMemo(
          () =>
            createBrowserRouter(
              createRoutesFromElements(
                <Route
                  path="/"
                  element={isSmallScreen ? <MobileLayout /> : <RootLayout />}
                >
                  {isSmallScreen && <Route index element={<MasterList />} />}
                  <Route path="page1" element={<Page1 />} />
                  <Route path="page2" element={<Page2 />} />
                  <Route path="page3" element={<Page3 />} />
                  <Route path="*" element={<Page4 />} />
                </Route>
              )
            ),
          [isSmallScreen]
        );
      
        return <RouterProvider router={router} />;
      }
      

    Alternative

    An alternative might be to utilize the useMatch hook to test if the root "/" is the currently matched route and render MasterList. The logic is a bit more convoluted and a little less DRY. The gist here is to render the normal MasterList and Outlet on larger view sizes, and on mobile views conditionally render MasterList only if on the home page, otherwise render the Outlet for the nested routes.

    Example:

    import { Outlet, useMatch } from 'react-router-dom';
    ...
    
    function RootLayout() {
      const isSmallScreen = useMediaQuery("(max-width: 768px)");
      const isHome = useMatch("/");
    
      return (
        <div
          style={{
            display: "flex",
            justifyContent: "space-around",
            alignItems: "flex-start"
          }}
        >
          {isSmallScreen ? (
            isHome ? (
              <MasterList />
            ) : (
              <div>
                <h3>Detail</h3>
                <Outlet />
              </div>
            )
          ) : (
            <>
              <MasterList />
              <div>
                <h3>Detail</h3>
                <Outlet />
              </div>
            </>
          )}
        </div>
      );
    }
    
    const router = createBrowserRouter(
      createRoutesFromElements(
        <Route path="/" element={<RootLayout />}>
          <Route path="page1" element={<Page1 />} />
          <Route path="page2" element={<Page2 />} />
          <Route path="page3" element={<Page3 />} />
          <Route path="*" element={<Page4 />} />
        </Route>
      )
    );
    
    export default function App() {
      return <RouterProvider router={router} />;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search