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 />
</>
)
}
Related
This question already has answers here:
How to create a protected route with react-router-dom?
(5 answers)
Closed 4 months ago.
I want to migrate from React Router V5 to V6, I used to map trough private routes and have a HOC that renders each private page components. I'm not sure how do it in same type of fashion for V6.
Here's how my Root component looked like:
const WrappedComponent = () => (
<Switch>
<Route exact path="/">
<Redirect to={routes.LOGIN} />
</Route>
<Route exact path={routes.LOGIN} component={Login} />
{privateRoutes.map((route) => (
<PrivateRoute
exact
component={route.component}
path={route.path}
key={route.path}
/>
))}
</Switch>
);
And here's how my PrivateRoute component looks like:
const PrivateRoute = ({ component: Component, ...props }) => {
const { loggedIn } = useSelector(({ auth }) => auth);
return (
<Route
render={(routerProps) =>
loggedIn ? (
<Component {...props} {...routerProps} />
) : (
<Redirect to={{ pathname: routes.LOGIN }} push />
)
}
/>
);
};
What is the way to map trough private routes and render them in React Router V6?
In react-router-dom v6 have Navigate (replacement of Redirect) and Outlet components. I would do
// PrivateRoute.tsx
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
export const PrivateRoute = () => {
const location = useLocation();
const auth = useAuth();
return auth.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" state={{ from: location }} replace />
);
};
and
// AppRouter.tsx
import { useRoutes } from 'react-router-dom';
import { PrivateRoute } from './PrivateRoute';
const AppRouter = () => {
const elements = useRoutes([
{ path: '/login', element: <LoginPage /> },
{ path: '/', element: <HomePage /> },
{
element: <PrivateRoute />,
children: [
{
path: '/',
element: <BasicLayout />,
children: [
{
path: 'sale',
// element: <SalePage />,
},
],
},
],
},
// Not found routes work as you'd expect
{ path: '*', element: <NotFoundPage /> },
]);
return elements;
};
export default AppRouter;
How can I use an array of routes for react-router-dom with components with the same props, without rendering the props to every routes seperatley?
const App = () => {
const list = [
{
path: `home`,
element: <Home />
},
{
path: `section`,
element: <Section />
},
];
return (
<BrowserRouter>
<Routes>
{list.map(list => <Route element={list.element ...{ myProp={test} } } path={list.path} )}
<Routes>
</BrowserRouter>
)
}
I'm sure there's a way to render these props inside my map (if I have the list in another file, for example).
Thanks!
Pass a reference to the component you want to render in the element property and create a locally scoped valid React component, i.e. a Capitalized/PascalCased variable. Don't forget to use a valid React key prop on the mapped elements.
Example:
const App = () => {
const list = [
{
path: 'home',
element: Home
},
{
path: 'section',
element: Section
},
];
return (
<BrowserRouter>
<Routes>
{list.map(list => {
const Element = list.element;
return (
<Route
key={list.path}
path={list.path}
element={<Element myProp={test} />}
/>
);
}}
<Routes>
</BrowserRouter>
);
};
Or you could just pass the prop when declaring the list.
const App = () => {
const list = [
{
path: 'home',
element: <Home myProp={test} />
},
{
path: 'section',
element: <Section myProp={test} />
},
];
return (
<BrowserRouter>
<Routes>
{list.map(list => <Route key={list.path} {...list} />)}
<Routes>
</BrowserRouter>
);
};
From here though you are just one more minor tweak away from just using the useRoutes hook and pass the list routes configuration.
Example:
import { useRoutes } from 'react-router-dom';
const App = () => {
const routes = useRoutes([
{
path: 'home',
element: <Home myProp={test} />
},
{
path: 'section',
element: <Section myProp={test} />
},
]);
return routes;
};
...
<BrowserRouter>
<App />
</BrowserRouter>
Try using Component instead of <Component />
const list = [
{
path: `home`,
element: Home
},
{
path: `section`,
element: Section
},
];
const App = () => {
return (
<BrowserRouter>
<Routes>
{list.map(list => <Route element={<list.element myProp={test} /> } path={list.path} )}
<Routes>
</BrowserRouter>
)
}
I also moved your list out of the component for performance reasons.
I have a list of pageNames and a list of pageUrls. I would like to create a router that generates a react component for each page in the list and uses a navbar with router links. I think the problem lies in the forEach function but I am not sure.
Can anyone tell what I am doing wrong?
Although I didn't find it helpful, here is a similar question.
import React, { useState } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
function App() {
const routes = [
{
name: 'home',
path: '/'
},
{
name: 'blog',
path: '/blog'
}
]
// translate (map) your array of objects into jsx
const routeComponents = routes.map(({name, path}) => ((
<Route key={name} path={path}>
<h1>{name}</h1>
</Route>
)))
return (
<div className="App" >
<Router>
<Navbar routes = {routes} />
<Switch>
{routeComponents}
</Switch>
</Router>
</div>
);
}
export default App;
function Navbar(props){
const links = props.routes.map(({name, path}) => (
<Link key={name} to={path}>{name}</Link>
))
return(
<div style={{justifyContent: "space-around", margin:"auto", display:"flex",justifyContent:"space-around",backgroundColor:"red"}}>
{links}
</div>
)
}
The issue you likely are seeing is due to the way the Switch component works. It renders the first matching Route or Redirect component. This means that within the Switch component path order and specificity matters! The home path "/" matches any path, so will always be matched and rendered.
You can avoid this by reordering the routes from more specific to less specific, or since you allude to wanting the routes to be dynamically generated from user interaction and stored in a state, specify the exact prop for all routes so all matching occurs exactly and the order and specificity is negated.
function App() {
const routes = [
{
name: 'home',
path: '/',
},
{
name: 'blog',
path: '/blog',
},
];
const routeComponents = routes.map(({name, path}) => (
<Route key={name} exact path={path}> // <-- exact prop to exactly match paths
<h1>{name}</h1>
</Route>
));
return (
<div className="App" >
<Router>
<Navbar routes={routes} />
<Switch>
{routeComponents}
</Switch>
</Router>
</div>
);
}
You're correct. forEach is populating your routes variable, which is an array. What you want is to build some JSX. This is traditionally done via map. You also do not need the useState calls, and you'll want to combine your page names and urls into a single object for easier management.
Here's some pseudocode that will put you on the path...
const routes = [
{
name: 'hello',
path: '/'
},
{
name: 'there',
path: '/there'
}
]
// translate (map) your array of objects into jsx
const routeComponents = routes.map(({name, path}) => (
<Route key={name} path={path}/>
))
// routeComponents is JSX. You originally had an array of JSX
<Switch>
{routeComponents}
</Switch>
// Same thing for your links
const navbar = routes.map(({name, path}) => (
<Link key={name} to={path}>{name}</Link>
))
<div>
{navbar}
</div>
update
some tweaks in above example to clarify it.
Update #2
Here's full code (jammed into 1 file).
Updated to react router v6.
test it here
import "./styles.css";
import React, { useState } from "react";
import {
BrowserRouter as Router,
Routes,
Route,
Link,
Outlet
} from "react-router-dom";
function Navbar({ routes }) {
const links = routes.map(({ name, path }) => (
<Link key={name} to={path}>
{name}
</Link>
));
return (
<div
style={{
justifyContent: "space-around",
margin: "auto",
display: "flex",
justifyContent: "space-around",
backgroundColor: "red"
}}
>
{links}
</div>
);
}
export default function App() {
const routes = [
{
name: "home",
path: "/"
},
{
name: "blog",
path: "/blog"
},
{
name: "about",
path: "/about"
}
];
// translate (map) your array of objects into jsx
const routeComponents = routes.map(({ name, path }) => (
<Route key={name} path={path} element={<h1>{name}</h1>} />
));
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Router>
<Routes>{routeComponents}</Routes>
<Navbar routes={routes} />
</Router>
{Outlet}
</div>
);
}
I am trying to build a simple router for my React application, which would consist of a few routes generated from an array of objects and a static 404 route. The requirement is that transitions between every route must be animated.
I am using react-router for the browser and react-transition-group.
I want to achieve something like this (stripped-down, incomplete pseudo-code):
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
{ path: "/contact", component: Contact }
];
const createRoute = (route) => {
return (
<CSSTransition className="page" timeout={300}>
<Route path={route.path} exact component={route.component} />
</CSSTransition>
);
}
<Router>
{routes.map(createRoute)}
<CSSTransition className="page" timeout={300}>
<Route component={PageNotFound} />
</CSSTransition>
</Router>
A full version of this code can be found on this Codesandbox:
https://codesandbox.io/s/react-router-switch-csstransition-catch-all-route-bug-forked-qzt9g
I need to use the <Switch> component to prevent the 404 route from showing in all the other routes of the application, but as soon as I add the <Switch>, the animations stop working.
In the example above, you will see that the animations don't work when used with <Switch>, despite following the guides from official docs of both react-router and react-transition-group.
However, they work perfectly without the use of the <Switch>, but then of course I end up with 404 route showing all the time.
Expected result:
animated transitions between all routes, those dynamically created as well as the static 404 page
Actual result:
no animations at all or animations with 404 route always showing
I have spent the entire day trying to find a solution to the problem that I encountered. Unfortunately I can't seem to find anything that would remotely help me fix the issue I'm facing, and I've searched on Google, Stack Overflow, Medium and finally I'm back here hoping someone can help me out please.
In order to have the animation working with the Switch component, you have to pass the right location with withRouter to the CSSTransition, coupled with TransitionGroup component.
I've modified you sandbox code with the following working solution:
import React from "react";
import ReactDOM from "react-dom";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import {
BrowserRouter as Router,
Route,
Switch,
Link,
withRouter
} from "react-router-dom";
import "./styles.css";
const rootElement = document.getElementById("root");
const components = {
Home: () => <div>Home page</div>,
About: () => <div>About the company</div>,
Contact: () => <div>Contact us</div>,
NotFound: () => <div>404</div>,
Menu: ({ links, setIsSwitch }) => (
<div className="menu">
{links.map(({ path, component }, key) => (
<Link key={key} to={path}>
{component}
</Link>
))}
<Link to="/404">404</Link>
</div>
)
};
const routes = [
{ path: "/", component: "Home" },
{ path: "/about", component: "About" },
{ path: "/contact", component: "Contact" }
];
const createRoutes = (routes) =>
routes.map(({ component, path }) => {
const Component = components[component];
const nodeRef = React.createRef();
return (
<Route key={path} path={path} exact>
{({ match }) => {
return (
<div ref={nodeRef} className="page">
<Component />
</div>
);
}}
</Route>
);
});
const AnimatedSwitch = withRouter(({ location }) => (
<TransitionGroup>
<CSSTransition
key={location.key}
timeout={500}
classNames="page"
unmountOnExit
>
<Switch location={location}>{createRoutes(routes)}</Switch>
</CSSTransition>
</TransitionGroup>
));
ReactDOM.render(
<React.StrictMode>
<Router>
<components.Menu links={routes} />
<AnimatedSwitch />
</Router>
</React.StrictMode>,
rootElement
);
This article explains in detail the reasoning behind it: https://latteandcode.medium.com/react-how-to-animate-transitions-between-react-router-routes-7f9cb7f5636a.
By the way withRouter is deprecated in react-router v6.
So you should implement that hook in your own.
import { useHistory } from 'react-router-dom';
export const withRouter = (Component) => {
const Wrapper = (props) => {
const history = useHistory();
return (
<Component
history={history}
{...props}
/>
);
};
return Wrapper;
};
See Deprecated issue discussion on GitHub: https://github.com/remix-run/react-router/issues/7156.
A quick fix is you can do following
<Switch>
<Route
path={"/:page(|about|contact)"}
render={() => createRoutes(routes)}
/>
<Route>
<div className="page">
<components.NotFound />
</div>
</Route>
</Switch>
Obviously not the most elegant nor scalable solution. But doable for a small react site.
Here's the forked codesandbox of working code.
https://codesandbox.io/s/react-router-switch-csstransition-catch-all-route-bug-forked-dhv39
EDIT: I was checking router.location.key first, but reaching through address bar was causing 404 page to render every page, so this is safer.
You can pass router as props to pages and conditionally render 404 page by checking router.location.pathname
export const routes = [
...all of your routes,
{
path: "*",
Component: NotFound,
},
]
{routes.map(({ path, pageTitle, Component }) => (
<Route key={path} exact path={path}>
{router => (
<CSSTransition
in={router.match !== null}
timeout={400}
classNames="page"
unmountOnExit
>
<div className="page">
<Component router={router} />
</div>
</CSSTransition>
)}
</Route>
))}
And in your 404 component, you need to check the path of the current route if it is available around all routes:
import { routes } from "../Routes";
const checkIfRouteIsValid = path => routes.findIndex(route => route.path === path);
export default function NotFound(props) {
const pathname = props.router.location.pathname;
const [show, setShow] = useState(false);
useEffect(() => {
setShow(checkIfRouteIsValid(pathname) === -1);
}, [pathname]);
return (
show && (
<div>Not found!</div>
)
);
}
I am able to navigate from /about to /portfolio. I can go to /portfolio/subItem1 or /portfolio/subItem2. I can click the back button to return to /portfolio. But I can not click the nav link while on any subitem route to go to /portfolio.
I suspect some sort of routing error, possibly between the top level router on App and the subrouter on the Portfolio component. I'm using react-router-dom 5.2.0.
// top level component
const bodyViews: Record<string, ComponentData> = {
cover: { name: "Home", slug: '/', component: Cover },
portfolio: { name: "Portfolio", slug: "/portfolio", component: Portfolio },
about: { name: "About", slug: '/about', component: About },
contact: { name: "Contact", slug: '/contact', component: Contact }
}
function App() {
return (
<OutermostStyleContainer>
<BrowserRouter>
<Nav bodyViews={bodyViews} />
<main>
<Switch>
<Route path='/about'><About /></Route>
<Route path='/contact'><Contact /></Route>
<Route path='/portfolio'><Portfolio /></Route>
<Route path='/' exact><Cover /></Route>
</Switch>
</main>
</BrowserRouter>
</OutermostStyleContainer>
);
}
// reduced portfolio component
interface PropsShape {}
export default (props: PropsShape) => {
return (
<BrowserRouter>
<section>
<Switch>
{/* Routes to sub-views */}
<Route path={`${useRouteMatch().path}/call-track-voipms`}>
<CallTrackVoipMs GithubLogo={GithubLogo} />
</Route>
{/* Nav links on category view */}
<ItemPreviews
previewData={typeCheckedNavigationData}
portfolioRoute={useRouteMatch().url}
/>
</Switch>
</section>
</BrowserRouter>
)
}
// full Nav component
export default (props: PropsShape) => {
return (
<nav>
<div>
<LinkList jsxData={props.bodyViews} />
</div>
</nav>
)
}
// LinkList. NavList is a styled HOC of <ul>
export default (props: { jsxData: Record<string, ComponentData> }) => {
return (
<section>
<NavList>
{ Object.values(props.jsxData).map((nameAndSlug: ComponentData) => (
<SingleLinkListItem linkData={nameAndSlug} />
)) }
</NavList>
</section>
)
}
// SingleListItem. StyledLink is as the name suggests
export default (props: { linkData: ComponentData }): JSX.Element => {
return (
<li>
<StyledLink to={props.linkData.slug}>{props.linkData.name}</StyledLink>
</li>
)
}
Does anyone see what is causing the routing issue?
I think you should not wrap the Portfolio component with <BrowserRouter>.
You should only wrap the top of the component with <BrowserRouter>.
And you can only wrap your child components with <Switch>.
// reduced portfolio component
interface PropsShape {}
export default (props: PropsShape) => {
return (
<section>
<Switch>
{/* Routes to sub-views */}
<Route path={`${useRouteMatch().path}/call-track-voipms`}>
<CallTrackVoipMs GithubLogo={GithubLogo} />
</Route>
{/* Nav links on category view */}
<ItemPreviews
previewData={typeCheckedNavigationData}
portfolioRoute={useRouteMatch().url}
/>
</Switch>
</section>
)
}