arunmmanoharan
arunmmanoharan

Reputation: 2675

Navigate inside a child route not working properly - React Router v6

I am using React Router v6 to build nested routes. I am facing 2 issues:

  1. If I click a link which has children, the url should automatically go to the child, but only the component is rendered. The URL still says "/*".
  2. Inside my child, I have a link which should get me the entire path. For example, it should be '/routeC/subC3/newRoute'

Please help.

This is my code.

App.js

import "./styles.css";
import {
  Navigate,
  Route,
  Routes,
  useMatch,
  useLocation,
  BrowserRouter,
  Link,
  Outlet
} from "react-router-dom";
import ComponentC from "./ComponentC";
import { Fragment } from "react";

const ComponentA = () => <p>Component A</p>;
const ComponentB = () => <p>Component B</p>;

const ComponentC1 = () => <p>I am in Component C1</p>;
const ComponentC2 = () => <p>I am in Component C2</p>;
const SubComponentC3 = () => <p>SubComponent C3</p>;

export const ComponentC3 = () => {
  const location = useLocation();
  const match = useMatch(location.pathname);
  return (
    <>
      <p>Component C3</p>
      <Link to={`${match.path}/newRoute`}>Take me to a new route</Link>
      <Routes>
        <Route
          exact
          path={`${match.path}/newRoute`}
          element={<SubComponentC3 />}
        />
      </Routes>
    </>
  );
};

export const componentCChildren = [
  {
    label: "Component C - 1",
    code: "subC1",
    component: ComponentC1
  },
  {
    label: "Component C - 2",
    code: "subC2",
    component: ComponentC2
  },
  {
    label: "Component C - 3",
    code: "subC3",
    component: ComponentC3
  }
];

export const routeValues = [
  {
    label: "Component A",
    path: "/routeA",
    component: ComponentA,
    children: []
  },
  {
    label: "Component B",
    path: "/routeB",
    component: ComponentB,
    children: []
  },
  {
    label: "Component C",
    path: "/routeC/*",
    component: ComponentC,
    children: componentCChildren
  }
];

export default function App() {
  return (
    <div className="App">
      <BrowserRouter>
        {routeValues.map((item) => (
          <Link key={item.path} to={item.path} style={{ paddingRight: "10px" }}>
            {item.label}
          </Link>
        ))}
        <Routes>
          {routeValues.map((route) => {
            if (route.children.length > 0) {
              return (
                <Route
                  key={route.path}
                  path={route.path}
                  element={<route.component />}
                >
                  {route.children.map((r, i, arr) => (
                    <Fragment key={r.code}>
                      <Route
                        path={`${route.path}/${r.code}`}
                        element={<r.component />}
                      />
                      <Route
                        path={route.path}
                        element={<Navigate to={`${route.path}/${arr[0].code}`} />}
                      />
                    </Fragment>
                  ))}
                </Route>
              );
            }

            return (
              <Route
                key={route.path}
                path={route.path}
                element={<route.component />}
              />
            );
          })}
          <Route path="*" element={<Navigate to="routeA" />} />
        </Routes>
        <Outlet />
      </BrowserRouter>
    </div>
  );
}

ComponentC.js

import { useState } from "react";
import Tab from "@mui/material/Tab";
import Box from "@mui/material/Box";
import TabContext from "@mui/lab/TabContext";
import TabList from "@mui/lab/TabList";
import TabPanel from "@mui/lab/TabPanel";
import { useNavigate, useMatch, useLocation } from "react-router-dom";

import { componentCChildren } from "./App";

export default function ComponentC(props) {
  const navigate = useNavigate();
  const location = useLocation();
  const match = useMatch(location.pathname);

  const [tabId, setTabId] = useState(componentCChildren[0].code);
  const handleTabChange = (e, tabId) => {
    console.log("tabId", tabId);
    navigate(`${tabId}`);
    setTabId(tabId);
  };

  return (
    <>
      <p>Component C</p>
      <TabContext value={tabId}>
        <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
          <TabList onChange={handleTabChange} aria-label="lab API tabs example">
            {componentCChildren.map((tab) => {
              return <Tab key={tab.code} value={tab.code} label={tab.label} />;
            })}
          </TabList>
        </Box>
        {componentCChildren.map((tab) => {
          return (
            <TabPanel key={tab.code} value={tab.code}>
              {<tab.component />}
            </TabPanel>
          );
        })}
      </TabContext>
    </>
  );
}

This is a link to my sandbox.

Upvotes: 2

Views: 2559

Answers (2)

Drew Reese
Drew Reese

Reputation: 202605

Here's a refactor that leaves most of your route definitions in tact. The changes are mostly in how, and where, the routes are rendered.

App.js

Remove the routeValues children and change the "/routeC/*" string literal to "/routeC" since it's used for both the route path and the link. Append the "*" wildcard character to the route's path when rendering.

ComponentC3 will use relative links and paths to get to ".../newRoute" where "..." is the currently matched route path.

export const ComponentC3 = () => {
  return (
    <>
      <p>Component C3</p>
      <Link to="newRoute">Take me to a new route</Link>
      <Routes>
        <Route path="newRoute" element={<SubComponentC3 />} />
      </Routes>
    </>
  );
};

export const routeValues = [
  {
    label: "Component A",
    path: "/routeA",
    component: ComponentA,
  },
  {
    label: "Component B",
    path: "/routeB",
    component: ComponentB,
  },
  {
    label: "Component C",
    path: "/routeC",
    component: ComponentC,
  }
];

export default function App() {
  return (
    <div className="App">
      <BrowserRouter>
        {routeValues.map((item) => (
          <Link key={item.path} to={item.path} style={{ paddingRight: "10px" }}>
            {item.label}
          </Link>
        ))}
        <Routes>
          {routeValues.map((route) => (
            <Route
              key={route.path}
              path={`${route.path}/*`} // <-- append wildcard '*' here
              element={<route.component />}
            />
          ))}
          <Route path="*" element={<Navigate to="routeA" />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

ComponentC.js

Here is where you'll render the componentCChildren as descendent routes. Within a new Routes component map componentCChildren to Route components each rendering a TabPanel component. Append the "*" wildcard matcher to the route path again so further descendent routes can be matched. Use a useEffect hook to issue an imperative redirect from "/routeC" to the first tab at "/routeC/subC1".

export default function ComponentC(props) {
  const navigate = useNavigate();

  useEffect(() => {
    if (componentCChildren?.[0]?.code) {
      // redirect to first tab if it exists
      navigate(componentCChildren[0].code, { replace: true });
    }
    // run only on component mount
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const [tabId, setTabId] = useState(componentCChildren[0].code);

  const handleTabChange = (e, tabId) => {
    console.log("tabId", tabId);
    navigate(tabId, { replace: true }); // just redirect between tabs
    setTabId(tabId);
  };

  return (
    <>
      <p>Component C</p>
      <TabContext value={tabId}>
        <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
          <TabList onChange={handleTabChange} aria-label="lab API tabs example">
            {componentCChildren.map((tab) => {
              return <Tab key={tab.code} value={tab.code} label={tab.label} />;
            })}
          </TabList>
        </Box>
        <Routes>
          {componentCChildren.map((tab) => {
            const TabComponent = tab.component;
            return (
              <Route
                key={tab.code}
                path={`${tab.code}/*`} // <-- append wildcard '*' here
                element={
                  <TabPanel value={tab.code}>
                    <TabComponent />
                  </TabPanel>
                }
              />
            );
          })}
        </Routes>
      </TabContext>
    </>
  );
}

Edit navigate-inside-a-child-route-not-working-properly-react-router-v6

Upvotes: 1

Kanti vekariya
Kanti vekariya

Reputation: 697

in ComponentC just you need to pass <Outlet />. i updated your working demo pls check here

Upvotes: 0

Related Questions