I have a page which has three routes. The 3rd route has a tab component which handles 3 sub routes. I am able to navigate to Route 3, but unable to view the tabs and unable to render the content under each tab.
Please advice.
This is my code:
import "./styles.scss";
import React, { useState } from "react";
import { Redirect, Route, Switch } from "react-router";
import { BrowserRouter, Link } from "react-router-dom";
import { Tab, Tabs } from "#blueprintjs/core";
const ComponentC1 = () => <p>Component C1</p>;
const ComponentC2 = () => <p>Component C2</p>;
const ComponentC3 = () => <p>Component C3</p>;
const componentCRoutes = [
{
label: "Component C - 1",
code: "subC1",
component: ComponentC1
},
{
label: "Component C - 2",
code: "subC2",
component: ComponentC2
},
{
label: "Component C - 3",
code: "subC3",
component: ComponentC3
}
];
const ComponentA = () => <p>Component A</p>;
const ComponentB = () => <p>Component B</p>;
const ComponentC = (props) => {
const [tabId, setTabId] = useState(componentCRoutes[0].label);
const handleTabChange = (tabId) => setTabId(tabId);
return (
<>
<p>Component C</p>
<Tabs onChange={handleTabChange} selectedTabId={tabId}>
{componentCRoutes.map((tab) => {
return (
<Tab
key={tab.code}
id={tab.label}
title={
<Link to={`/${props.match.url}/${tab.code}`}>{tab.label}</Link>
}
/>
);
})}
</Tabs>
{(() => {
const { component, code } = componentCRoutes.find(
(item) => item.label === tabId
);
return (
<Route path={`${props.match.url}/${code}`} component={component} />
);
})()}
<Route exact path={props.match.url}>
<Redirect to={`${props.match.url}/${componentCRoutes[0].code}`} />
</Route>
</>
);
};
const routes = [
{ 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">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<BrowserRouter>
{routes.map((item) => (
<Link key={item.path} to={item.path} style={{ paddingRight: "10px" }}>
{item.label}
</Link>
))}
<Switch>
{routes.map((route) => {
return (
<Route
key={route.path}
exact
path={route.path}
component={route.component}
/>
);
})}
<Route exact path="/">
<Redirect to="/routeA" />
</Route>
</Switch>
</BrowserRouter>
</div>
);
}
This is my codesandbox link
Please advice.
Issues:
Tabs in ComponentC are not working correctly as React-Router Link. It can be fixed using history.push in Tab's onChange handler.
You have not defined Routes in your nested component properly. You are using find to define the Route, that looks dirty. It can be fixed using a Switch and Route in nested component i.e. ComponentC
You used Route and Redirect to make default paths. That can be simplified as well.
You used props.match.url and props.match.path incorrectly. props.match.url (URL) should be used in Link or history.push and props.match.path (PATH) should be used in path of your nested Routes declarations.
Solution:
After fixing all the issues mentioned above, Here is the working code:
(Also, note that the Route that has nested routes should not be marked exact={true})
Main Routes:
const routes = [
{ exact: true, label: "Component A", path: "/routeA", component: ComponentA },
{ exact: true, label: "Component B", path: "/routeB", component: ComponentB }
{ exact: false, label: "Component C", path: "/routeC", component: ComponentC }
// ^ it is false because it has nested routes
];
// JSX
<BrowserRouter>
{routes.map((item) => (
<Link key={item.path} to={item.path}>
{item.label}
</Link>
))}
<Switch>
{routes.map((route) => {
return (
<Route
key={route.path}
exact={route.exact}
path={route.path}
component={route.component}
/>
);
})}
<Redirect exact from="/" to="/routeA" />
</Switch>
</BrowserRouter>
And Here is nested routes declarations inside ComponentC:
const routes = [
{
label: "Component C1",
code: "subC1",
component: ComponentC1
},
{
label: "Component C2",
code: "subC2",
component: ComponentC2
},
{
label: "Component C3",
code: "subC3",
component: ComponentC3
}
];
export default function ComponentC(props) {
const [tabId, setTabId] = useState(routes[0].code);
const handleTabChange = (tabId) => {
props.history.push(`${props.match.url}/${tabId}`);
setTabId(tabId);
};
return (
<>
<Tabs onChange={handleTabChange} selectedTabId={tabId}>
{routes.map((tab) => {
return <Tab key={tab.code} id={tab.code} title={tab.label} />;
})}
</Tabs>
<Switch>
{routes.map((route) => (
<Route
key={route.code}
exact
path={`${props.match.path}/${route.code}`}
component={route.component}
/>
))}
<Redirect
exact
from={props.match.url}
to={`${props.match.url}/${routes[0].code}`}
/>
</Switch>
</>
);
}
Here is full demo on Sandbox.
Related
I am using React Router v6 to build nested routes. I am facing 2 issues:
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 "/*".
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.
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>
</>
);
}
in ComponentC just you need to pass <Outlet />. i updated your working demo pls check here
Following up from my question React router v6 and relative links from page within route, I'm trying to refactor the routes in our app to be more nested.
Trouble is that it doesn't seem possible to render a Route element recursively from data, because react-router insists that Route is directly inside Route and not wrapped in another component, and I cannot see how to render recursively (to arbitrary depth) any other way.
Reproduction on codesandbox.
import React from "react";
import { BrowserRouter, Routes, Route} from "react-router-dom";
import "./styles.css";
function GenericPage() {
return <div className="page">Generic page</div>;
}
const nav = {
slug: "",
title: "Home",
children: [
{
slug: "foo",
title: "Foo"
},
{
slug: "bar",
title: "Bar"
}
]
};
const RecursiveRoute = ({ node }) => {
return (
<Route path={node.slug} element={<GenericPage />}>
{node.children?.map((child) => (
<RecursiveRoute node={child} />
))}
</Route>
);
};
export default function App() {
return (
<BrowserRouter>
<Routes>
<RecursiveRoute node={nav} />
</Routes>
</BrowserRouter>
);
}
Error from react-router:
[RecursiveRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
Issue
As the error indicates, you can't render Route components directly, they must be rendered directly by a Routes component, or another Route component in the case of nested routes.
Solution
Refactor RecursiveRoute to render a Routes component with a route for the current node and then map the node's children to routes that render the RecursiveRoute as an element.
Example:
function GenericPage({ title }) {
return (
<div className="page">
{title} page
</div>
);
}
const RecursiveRoute = ({ node }) => (
<Routes>
<Route
path={`${node.slug}/*`}
element={<GenericPage title={node.title} />}
/>
{node.children?.map((child) => (
<Route
key={child.slug}
element={<RecursiveRoute key={child.slug} node={child} />}
/>
))}
</Routes>
);
Suggestion
I strongly suggest not trying to roll your own custom route configuration and renderer, use the useRoutes hook instead to do all the heavy lifting for you.
Example:
Refactor the navigation config:
const nav = [
{
path: "/",
element: <GenericPage title="Home" />,
children: [
{
path: "foo",
element: <GenericPage title="Foo" />
},
{
path: "bar",
element: <GenericPage title="Bar" />
}
]
}
];
Pass the config to the useRoutes hook and render the result:
const routes = useRoutes(nav);
...
return routes;
Demo
I had the same puzzle to solve. In general I solve it by passing a function in Routes component. Here is my solution with few code snippets.
// in Routes.ts
interface IRoutes {
path: string
component: JSX.Element
children?: IRoutes[]
}
const routes: IRoutes[] = [
{
path: 'warehouse'
component: <WarehousePage />
children: [
{
path: 'products'
component: <ProductsPage />
},
{
path: 'units'
component: <UnitsPage />
},
]
},
]
// in AppRouter.tsx
const renderRoutesRecursive = (routes: IRoutes[]) =>
routes.map((route, index) =>
route.children ? (
renderRoutesRecursive(route.children)
) : (
<Route
key={index}
path={route.path}
element={route.component}
/>
),
)
const renderRoutes = useMemo(() => renderRoutesRecursive(routes), [routes])
return (
<Routes>
<Route path='/' element={<Layout />}>
{renderRoutes}
</Route>
</Routes>
)
// in Layout.tsx
const Layout = () => {
return (
<>
<Header />
<Navigation />
<Main>
<Outlet />
</Main>
<Footer />
</>
)
}
Routes:
const issueSubmissionModal = () => IssueModal({ isVisible: true })
const renderHeader = () => <AppHeader userData={userData}><AppNavbar/></AppHeader>
const renderHome = () => <AppTemplate userData={userData} />
const Routes = userData ? [
{
key: "home",
path: "/",
component: renderHome(),
includeProfile: null,
includeAppheader: renderHeader()
},
{
key: "issueSubmission",
path: "/issueSubmission",
component: issueSubmissionModal(),
includeProfile: renderHome(),
includeAppheader: renderHeader()
},
] : []
const renderRoutes = () => <Switch>
{Routes.map(route => (
<Route exact path={route.path} key={route.key}>
{route.includeProfile && route.includeProfile}
{route.component}
</Route>
))}
</Switch>
return renderRoutes();
App.tsx:
function App() {
const { data: userData, isLoading, isError, error } = useAppUser();
return (
<div className="App">
<Router>
{userData ?
<Routes userData={userData} />
: null}
</Router>
</div>
);
}
export default App;
Problem? No matter what I do, all routes render as soon as the page launches. I simplified to two routes here, but the actual app has several.
It seems to me Switch isn't working. I don't know why.
I can go back to just having all the routes written out manually in App.tsx, but that's gross and inefficient for what we're trying to do.
Any React gurus got an idea?
Probably you are missing to include component tag within <Route />
{
Routes.map(route => (
<Route exact path={route.path} key={route.key} component=
{route.includeProfile ? route.component} />
)
)
}
Guys anyone would know how to help me how to use Child routes with Lazy loading with react-router.
I'm doing it the way below but when I try to access the sub route it doesn't work
-- the route is declared as a array
import { lazy } from 'react'
const routesLIst = [
{
path: '/myPage',
name: 'myPage',
Component: lazy(() => import('src/pages/myPage')),
children: [
{ path: '/test', name: 'test', Component: lazy(() => import('src/components/myPage/test')) },
{ path: '/test2', name: 'test2', Component: lazy(() => import('src/components/myPage/test2')) },
]
}
-in the page I'm trying to acces the data with props.children, but the sub-route doesn't work
{ props.children }
example: I'm trying to access myPage/test , and get the data inside the myPage with props.children
const myPage: FC<> = (props) => {
const { children } = props
return (
<div>
{ children }
</div>
)
}
import {Route, Switch} from 'react-router';
interface Routes {
routes?: any
}
export const Routes: FC<Routes> = (props) => {
const {routes} = props
return (
<>
<Switch>
{routes.map((route) => {
if (!route.children || route.children.length < 1) {
return (
<Route
key={route.path}
exact={route.exact}
path={route.path}
render={props => (<route.Component {...props} name={route.name}/>)}
/>
)
} else {
return (
<Route
key={route.path}
exact={route.exact}
path={route.path}
render={props => (
<route.Component {...props} name={route.name}>
{
route.children.map((subRoute) => (
<Route
key={subRoute.path}
exact={subRoute.exact}
path={subRoute.path}
render={props => (
<subRoute.Component {...props} name={subRoute.name}/>
)}
/>
))
}
</route.Component>
)
}
/>
)
}
}
)}
</Switch>
</>
);
};
I have a react router app:
export default () => (
<Router basename={process.env.REACT_APP_BASENAME || ""}>
<div>
{routes.map((route, index) => {
return (
<PrivateRoute
key={index}
path={route.path}
exact={route.exact}
component={props => {
return (
<route.layout {...props}>
<route.component {...props} />
</route.layout>
);
}}
/>
);
})}
</div>
</Router>
);
and this will render dfferent views based on the route clicked. the routes will render based on this object in a routes.js file:
export default [
{
path: "/login",
layout: DefaultLayout,
component: LogIn
}, .....]
To build in some authentication, I defined a PrivateRoute as:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
fakeAuth.isAuthenticated === true
? <Component {...props} />
: <Redirect to='/login' />
)} />
)
however, when i set the app as using PrivateRoute instead of normal Route (in the first snippet), the redirect does not use the routes object. How do I change the PrivateRoute const for a log in page reflect my original React Route architecture? what is the best practice?
Your code looks fine, but since you said your routes object is not understood by react-router maybe there is the case that your components aren't defined properly. For example, your components may be defined after the object is created. In that case, when that object is created, it will refer to undefined components. I made this mistake once, so I am just sharing what possibly went wrong.
Here is an example:
import ReactDOM from "react-dom";
import React, { Component } from "react";
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
withRouter
} from "react-router-dom";
function Public() {
return <h3>Public</h3>;
}
function Protected() {
return <h3>You can see protected content</h3>;
}
class Login extends Component {
state = { redirectToReferrer: false };
login = () => {
fakeAuth.authenticate(() => {
this.setState({ redirectToReferrer: true });
});
};
render() {
let { from } = this.props.location.state || { from: { pathname: "/" } };
let { redirectToReferrer } = this.state;
if (redirectToReferrer) return <Redirect to={from} />;
return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={this.login}>Log in</button>
</div>
);
}
}
const routes = [
{
path: "/public",
component: Public,
private: false
},
{
path: "/login",
component: Login,
private: false
},
{
path: "/protected",
component: Protected,
private: true
}
];
function AuthExample() {
return (
<Router>
<div>
<AuthButton />
<ul>
<li>
<Link to="/public">Public Page</Link>
</li>
<li>
<Link to="/protected">Protected Page</Link>
</li>
</ul>
{routes.map((route, index) => {
if (route.private)
return (
<PrivateRoute
key={index}
path={route.path}
exact={route.exact}
component={props => {
return <route.component {...props} />;
}}
/>
);
return (
<Route
key={index}
path={route.path}
exact={route.exact}
component={props => {
return <route.component {...props} />;
}}
/>
);
})}
</div>
</Router>
);
}
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
this.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
this.isAuthenticated = false;
setTimeout(cb, 100);
}
};
const AuthButton = withRouter(({ history }) =>
fakeAuth.isAuthenticated ? (
<p>
Welcome!{" "}
<button
onClick={() => {
fakeAuth.signout(() => history.push("/"));
}}
>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
)
);
function PrivateRoute(props) {
const { component: Component, ...rest } = props;
return (
<Route
{...rest}
render={props =>
fakeAuth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<AuthExample />, rootElement);
Notice Public, Protected and Login components are defined above the routes object. Defining them after the routes will result in errors.
I suggest to change your private route as following
const PrivateRoute = ({ component: Component, ...rest }) => fakeAuth.isAuthenticated === true ? (
<Route {...rest} component={component}
)} />
) : <Redirect to='/login' />;