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
Related
For some files in my project, VS Code highlights HTML parts of code as errors, but the project builds and runs fine. So there are no actual errors, but VS Code says that almost every line has an error.
Other examples are:
'UserProvider' refers to a value, but is being used as a type here.
Did you mean 'typeof UserProvider'? ts(2749)
'>' expected.ts(1005)
...
Here is a sample of this code that creates this mess.
import React from "react";
import "./App.css";
import { UserConsumer, UserProvider } from "./contexts/UserContext";
import { Routes, Route, Link, Navigate } from "react-router-dom";
import { Button, Layout, Menu, Spin } from "antd";
import {
DashboardOutlined,
DollarOutlined,
HomeOutlined,
LogoutOutlined,
WarningOutlined
} from "#ant-design/icons";
import FoodEntries from "./components/FoodEntries";
import Login from "./components/Login";
import CalorieLimit from "./components/CalorieLimit";
import SpendLimit from "./components/SpendLimit";
import Admin from "./components/Admin";
const { Sider } = Layout;
const App = () => {
return (
<UserProvider>
<UserConsumer>
{({ logout, authenticated, isAdmin, loadingAuthState }) => (
<div className="App">
<Spin tip="Loading..." spinning={loadingAuthState}>
<Layout>
{authenticated && (
<Sider
breakpoint="lg"
collapsedWidth="0"
onBreakpoint={broken => {
console.log(broken);
}}
onCollapse={(collapsed, type) => {
console.log(collapsed, type);
}}
>
<Menu
theme="light"
mode="inline"
defaultSelectedKeys={["home"]}
items={[
{
key: "home",
icon: React.createElement(HomeOutlined),
label: <Link to="/">Food Entries</Link>
},
{
key: "calories",
icon: React.createElement(WarningOutlined),
label: <Link to="/calories">Calorie Limit</Link>
},
{
key: "spend",
icon: React.createElement(DollarOutlined),
label: <Link to="/spend">Spend Limit</Link>
},
isAdmin
? {
key: "admin",
icon: React.createElement(DashboardOutlined),
label: <Link to="/admin">Admin</Link>
}
: null,
{
type: "divider"
},
{
key: "logout",
danger: true,
icon: React.createElement(LogoutOutlined),
label: (
<Button onClick={logout} type="link">
Logout
</Button>
)
}
]}
/>
</Sider>
)}
<Routes>
<Route path="/" element={<FoodEntries />} />
<Route path="/calories" element={<CalorieLimit />} />
<Route path="/spend" element={<SpendLimit />} />
<Route path="/admin" element={<Admin />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<Navigate to="/login" />} />
</Routes>
</Layout>
</Spin>
</div>
)}
</UserConsumer>
</UserProvider>
);
};
export default App;
I'm trying to move from having current active companyId in query params to URL path.
http://localhost:3000/1?companyId=comp1
http://localhost:3000/2?companyId=comp1
To
http://localhost:3000/comp1/1
http://localhost:3000/comp1/2
So basically:
User Auth it's self in Login screen.
Then selects a company from list to work with / see data in Page1 and Page2
With specific company selected Page1 and Page2 should be loaded, and that company selected should continue to be in URL
http://localhost:3000/comp1/1
http://localhost:3000/comp1/2
Initial idea i had is to store selected company in state and then on state change change the basename but that does not work, i can't access Page*
Main App
import { Link, BrowserRouter as Router, Switch } from 'react-router-dom'
import { useStoreRoutes } from '../../states/useStoreRoutes'
import { PrivateRoutes } from './components/PrivateRoutes'
import { PublicRoutes } from './components/PublicRoutes'
export const ReactRouter = () => {
const currentOrg = useStoreRoutes((s) => s.currentOrg)
return (
<>
<Router basename={process.env.PUBLIC_URL + currentOrg}>
<div style={{ display: 'flex', gap: '10px' }}>
<Link to="/">Home</Link>
<Link to="/login">Login</Link>
<Link to="/compList">Comp List</Link>
<Link to="/1">Page 1</Link>
<Link to="/2">Page 2</Link>
</div>
<RoutesComp />
</Router>
</>
)
}
const RoutesComp = () => {
return (
<Switch>
<PrivateRoutes component={Page1} path="/1" />
<PrivateRoutes component={Page2} path="/2" />
<PublicRoutes component={Login} path="/login" />
<PublicRoutes component={CompList} path="/compList" />
<PublicRoutes component={Home} path="/" />
</Switch>
)
}
const Home = () => <h2>Home</h2>
const Login = () => <h2>Login</h2>
const CompList = () => {
const currentOrgSet = useStoreRoutes((s) => s.currentOrgSet)
return (
<h2>
<Link to="/comp1" onClick={() => currentOrgSet('comp1')}>
Comp1
</Link>
<Link to="/comp2" onClick={() => currentOrgSet('comp2')}>
Comp2
</Link>
</h2>
)
}
const Page1 = () => <h2>Page 1</h2>
const Page2 = () => <h2>Page 2</h2>
State
import create from 'zustand'
export const useStoreRoutes = create((set: any) => {
return {
currentOrg: '',
currentOrgSet: (data: string) => set({ currentOrg: data }),
}
})
PrivateRoutes
import { Redirect, Route, RouteProps } from 'react-router-dom'
export const db = [
{
companyId: 'comp1',
},
{
companyId: 'comp2',
},
{
companyId: 'comp3',
},
]
export const PrivateRoutes = ({ component: Component, ...rest }: any) => {
const getCompId = window.location.pathname.split('/')[1]
const checkIfCompIsInUser = () => {
for (let i = 0; i < db.length; i++) {
const el = db[i]
if (el.companyId === getCompId) {
console.log(true)
return true
} else {
console.log(false)
return false
}
}
}
return (
<Route
render={(props: any) =>
checkIfCompIsInUser() ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
{...rest}
/>
)
}
PublicRoute
import { Route, RouteProps } from 'react-router-dom'
export const PublicRoutes = ({ component: Component, ...rest }: any) => {
return <Route {...rest} render={(props: any) => <Component {...props} />} />
}
From what I can tell, you need to declare the Page1 and Page2 routes to use a dynamic path that includes the companyId.
const RoutesComp = () => {
return (
<Switch>
<PrivateRoutes component={Page1} path="/:companyId/1" />
<PrivateRoutes component={Page2} path="/:companyId/2" />
<PublicRoutes component={Login} path="/login" />
<PublicRoutes component={CompList} path="/compList" />
<PublicRoutes component={Home} path="/" />
</Switch>
);
};
From here you can fix the PrivateRoutes custom route component to read the route match params for checking access.
export const PrivateRoutes = ({ component: Component, ...rest }: any) => {
return (
<Route
render={(props: any) => {
const { companyId } = props.match.params;
const checkIfCompIsInUser = () => {
return db.some((el) => el.companyId === companyId);
};
return checkIfCompIsInUser() ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
);
}}
{...rest}
/>
);
};
Example CompanyList to render links to each company's page 1 and 2.
const CompList = () => {
return (
<ul>
<li>
<Link to="/comp1/1">Comp1</Link>
</li>
<li>
<Link to="/comp1/2">Comp2</Link>
</li>
<li>
<Link to="/comp2/1">Comp1</Link>
</li>
<li>
<Link to="/comp2/2">Comp2</Link>
</li>
</ul>
);
};
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.
Codesandbox
Hi, if I create an array of routes with React Router which has the HOC of <MainRoute /> which wraps the actual <Component /> with <Main/>, on every path change <Main/> is getting remounted and hence the useEffect hook is getting recalled.
Is there a way to only remount the <Component /> not <Main/> itself? Maybe some kind of memoization?
Mapping over routes is very convenient, however it seems to re-map everything on the location change which doesn't happen if I just hardcode every route like <MainRoute path=.. component=.. /> inside the <Switch />.
Help highly appreciated,
Cheers
import React from "react";
import { Route, Switch, BrowserRouter, Link } from "react-router-dom";
import styled from "styled-components";
const Layout = styled.div`
margin: auto;
width: 0;
`;
const Main = ({ children, ...props }) => {
React.useEffect(() => {
console.log("API REQUEST - Called every time");
}, []);
return <Layout>{React.cloneElement(children, props)}</Layout>;
};
const MainRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props => {
return (
<Main path={rest.path}>
<Component {...props} />
</Main>
);
}}
/>
);
};
export default function App() {
return (
<BrowserRouter>
<Switch>
{routes.map(({ path, component }) => (
<MainRoute key={path} path={path} component={component} exact />
))}
</Switch>
</BrowserRouter>
);
}
const Home = () => (
<>
Home <Link to="/other">Other</Link>
</>
);
const Other = () => (
<>
Other <Link to="/">Home</Link>
</>
);
const routes = [
{ path: "/", component: Home },
{ path: "/other", component: Other }
];
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' />;