Split up routes in same router with reach-router - reactjs

Say I have a lot of routes and I would like to split them up in groups.
How would I accomplish this in React with Reach Router?
Example of what I'm basically trying to accomplish:
const Router = () => {
return (
<Router>
<AnonymousRoutes />
<SecureRoutes />
</Router>
);
};
const AnonymousRoutes = () => {
return (
<>
<Page1 path="1" />
<Page2 path="2" />
</>
);
};
const SecureRoutes = () => {
return (
<>
<Page3 path="3" />
<Page4 path="4" />
</>
);
};

Edit: So, I based my answer off of a misreading of your problem statement. I thought I read react-router, not reach router. I apologize.
So using a fragment is exactly what you probably SHOULD be doing. However, React Reach doesn't currently support fragments. Silver lining, it looks like it will soon!
https://github.com/reach/router/pull/289
If I'm understanding your question correctly, I think what you're looking for is Switch from react-router.
The switch component allows a developer to segment out their routes and render specific content on the path.
It might look something like this:
import { Switch, Route } from 'react-router'
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
</Switch>

Related

Which PrivateRouter realization is better: higher-order component or substitution?

So recently I found out two ways of creating private routes in react.
With a HOC (higher-order component):
const PrivateRoute = ({ user, children }) => {
if (!user) {
return <Navigate to="/home" replace />;
}
return children;
};
const App = () => {
...
return (
<>
...
<Routes>
<Route path="/home" element={<Home />} />
<Route
path="/privateroute"
element={
<PrivateRoute user={user}>
<PrivateComponent />
</PrivateRoute >
}
/>
...
</Routes>
</>
);
};
With substituting routes completely
const App = () => {
...
return (
<>
{user ? (
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/privateroute" element={<PrivateComponent />} />
...
</Routes>
) : (
<Routes>
<Route path="/home" element={<Home />} />
...
</Routes>
)}
</>
);
}
My fellow colleague told me that the second way is quite bad since it completely erases some routes (if user is falsy then there is no route to /privateroute). But on my question why might that be bad he had no definitive answer. I couldn't find anything on the internet either. Any thoughts on which way is the best?
Between these two options, the first is the preferred solution since it keeps all routes mounted so they there will be no race condition between setting the user state and issuing an imperative navigation action to one of the protected routes. In other words, with the second implementation you have to wait for the user state to update and trigger a component rerender so the protected routes are mounted and available to be navigated to.
The second method also duplicates unauthenticated routes if it's all one or the other. Code duplication should be avoided.
Note however though that the first example isn't a Higher Order Component, it's just a wrapper component.
Note also that it's more common to create a PrivateRoute component as a Layout Route instead of as a Wrapper component. The change is trivial but it makes the component a little more wieldy. Render an Outlet component for nested routes instead of the children prop for a single wrapped child component.
import { ..., Outlet } from 'react-router-dom';
const PrivateRoute = ({ user }) => {
return user ? <Outlet /> : <Navigate to="/home" replace />;
};
Now instead of wrapping each individual route you want to protect you render a layout route that wraps an entire group of routes you want to protect. It makes your code more DRY.
const App = () => {
...
return (
<>
...
<Routes>
<Route path="/home" element={<Home />} />
... other unprotected routes ...
<Route element={<PrivateRoute />}>
<Route path="/privateroute" element={<PrivateComponent />} />
... other protected routes ...
</Route>
... other unprotected routes ...
</Routes>
</>
);
};

react-router redirect doesn't change url in Switch

I'm using react-router to set up my application, and try to use <Redirect/> to set router for authentication.
Routecomponent have two different components, one is private Route, another is public route.
Expect result : when auth is false, the page should jump back to a public page, which I set <Redirect to={"/public"} />
so far it seems route works fine, but redirect doesn't work properly.
Any ideas are welcome! Thanks!!
PrivateRoute
interface PrivateRouteProps {
isLogin: boolean;
privateRoutes: RouteItem[];
}
const PrivateRoute: React.FunctionComponent<PrivateRouteProps> = (
props: PrivateRouteProps
) => {
return (
<>
{props.isLogin ? (
props.privateRoutes.map(item => {
return <Route key={item.path} {...item} />;
})
) : (
<Redirect to={PUBLIC.path} />
)}
</>
);
};
PublicRoute
interface PublicProps {
publicRoutes: RouteItem[];
}
const PublicRoute: React.FC<PublicProps> = (props: PublicProps) => {
return (
<>
{props.publicRoutes.map(route => (
<Route key={route.path} {...route} />
))}
</>
);
};
Route
<BrowserRouter>
<Switch>
<PublicRoute publicRoutes={publicRoutes} />
<PrivateRoute privateRoutes={privateRoutes} isLogin={login} />
</Switch>
</BrowserRouter>
UPDATE
As the accepted answer mentioned, it's all about <Switch/> works with Fragment, so I modified my routes as following, it works like a charm.
Just update it for someone may have similar question.
<BrowserRouter>
<Switch>
{publicRoutes.map(item => {
return <Route key={item.path} {...item}/>
})}
{privateRoutes.map(item => {
return <PrivateRoute key={item.path}
exact={item.exact}
component={item.component}
path={item.path}
redirectPath={SIGN_IN.path}
/>
})}
</Switch>
</BrowserRouter>
I have gone through your code and boils down to one thing. The way that the component <Switch> works with fragment <></>. It only looks for the first React Fragment because they do not want to transverse a tree:
https://github.com/ReactTraining/react-router/issues/5785
To solve that you need to either remove the React.Fragment inside your components.
So your application will look like:
<Switch>
<Route ...>
<Route ...>
<Route ...>
</Switch>
and NOT (btw that is how it is now)
<Switch>
//it will only sees the first one //if you switch orders - Private with Public
// Private will work but not Public anymore :(
<React.Fragment>
<Route ...>
<Route ...>
</React.Fragment>
<React.Fragment>
<Route ...>
</React.Fragment>
</Switch>
Another solution (that is what I did because I am not well versed in TypeScript enough to change types and returns) is to add a wrapper in your switch application and deal with the return of the private Routes using the render method inside the <Route> as demonstrated below:
//index.tsx
<Switch>
<>
<PublicRoute publicRoutes={publicRoutes}/>
<PrivateRoute privateRoutes={privateRoutes} isLogin={login}/>
</>
</Switch>
That leads to another error of infinite loops re-renders (again the react-router is probably having a bad time with the nested routes) and to solve that you would do the following to your PrivateRoutes component:
//PrivateRoute.tsx
return (
<>
{props.privateRoutes.map(item =>
<Route key={item.path} exact path={item.path} render={() => (
!props.isLogin
? (
<Redirect to={PUBLIC.path}/>
):
//HOC transforming function Component into Component
// #ts-ignore (you can deal here better than me hehehe)
((PrivateComponent)=><PrivateComponent/>)(item.component)
)}/>)}
</>
);
TL,DR: You are adding nesting complexity by adding <></> (translates to React.Fragment) inside your structure. If you remove them or follow the code above you should be fine
Hope I have helped it you. Good luck! :)

React-Router - Route re-rendering component on route change

Please read this properly before marking as duplicate, I assure you I've read and tried everything everyone suggests about this issue on stackoverflow and github.
I have a route within my app rendered as below;
<div>
<Header compact={this.state.compact} impersonateUser={this.impersonateUser} users={users} organisations={this.props.organisations} user={user} logOut={this.logout} />
<div className="container">
{user && <Route path="/" component={() => <Routes userRole={user.Role} />} />}
</div>
{this.props.alerts.map((alert) =>
<AlertContainer key={alert.Id} error={alert.Error} messageTitle={alert.Error ? alert.Message : "Alert"} messageBody={alert.Error ? undefined : alert.Message} />)
}
</div>
The route rendering Routes renders a component that switches on the user role and lazy loads the correct routes component based on that role, that routes component renders a switch for the main pages. Simplified this looks like the below.
import * as React from 'react';
import LoadingPage from '../../components/sharedPages/loadingPage/LoadingPage';
import * as Loadable from 'react-loadable';
export interface RoutesProps {
userRole: string;
}
const Routes = ({ userRole }) => {
var RoleRoutesComponent: any = null;
switch (userRole) {
case "Admin":
RoleRoutesComponent = Loadable({
loader: () => import('./systemAdminRoutes/SystemAdminRoutes'),
loading: () => <LoadingPage />
});
break;
default:
break;
}
return (
<div>
<RoleRoutesComponent/>
</div>
);
}
export default Routes;
And then the routes component
const SystemAdminRoutes = () => {
var key = "/";
return (
<Switch>
<Route key={key} exact path="/" component={HomePage} />
<Route key={key} exact path="/home" component={HomePage} />
<Route key={key} path="/second" component={SecondPage} />
<Route key={key} path="/third" component={ThirdPage} />
...
<Route key={key} component={NotFoundPage} />
</Switch>
);
}
export default SystemAdminRoutes;
So the issue is whenever the user navigates from "/" to "/second" etc... app re-renders Routes, meaning the role switch logic is rerun, the user-specific routes are reloaded and re-rendered and state on pages is lost.
Things I've tried;
I've tried this with both react-loadable and React.lazy() and it has the same issue.
I've tried making the routes components classes
Giving all Routes down the tree the same key
Rendering all components down to the switch with path "/" but still the same problem.
Changing Route's component prop to render.
Changing the main app render method to component={Routes} and getting props via redux
There must be something wrong with the way I'm rendering the main routes component in the app component but I'm stumped, can anyone shed some light? Also note this has nothing to do with react-router's switch.
EDIT: I've modified one of my old test project to demonstrate this bug, you can clone the repo from https://github.com/Trackerchum/route-bug-demo - once the repo's cloned just run an npm install in root dir and npm start. I've got it logging to console when the Routes and SystemAdminRoutes are re-rendered/remounted
EDIT: I've opened an issue about this on GitHub, possible bug
Route re-rendering component on every path change, despite path of "/"
Found the reason this is happening straight from a developer (credit Tim Dorr). The route is re-rendering the component every time because it is an anonymous function. This happens twice down the tree, both in App and Routes (within Loadable function), below respectively.
<Route path="/" component={() => <Routes userRole={user.Role} />} />
needs to be
<Routes userRole={user.Role} />
and
loader: () => import('./systemAdminRoutes/SystemAdminRoutes')
Basically my whole approach needs to be rethought
EDIT: I eventually fixed this by using the render method on route:
<Route path="/" render={() => <Routes userRole={user.Role} />} />
Bumped into this problem and solved it like this:
In the component:
import {useParams} from "react-router-dom";
const {userRole: roleFromRoute} = useParams();
const [userRole, setUserRole] = useState(null);
useEffect(()=>{
setUserRole(roleFromRoute);
},[roleFromRoute]}
In the routes:
<Route path="/generic/:userRole" component={myComponent} />
This sets up a generic route with a parameter for the role.
In the component useParams picks up the changed parameter und the useEffect sets a state to trigger the render and whatever busines logic is needed.
},[userRole]);
Just put the "/" in the end and put the other routes above it.
Basically it's matching the first available option, so it matches "/" every time.
<Switch>
<Route key={key} exact path="/home" component={HomePage} />
<Route key={key} path="/second" component={SecondPage} />
<Route key={key} path="/third" component={ThirdPage} />
<Route key={key} exact path="/" component={HomePage} />
<Route key={key} component={NotFoundPage} />
</Switch>
OR
<Switch>
<Route path="/second" component={SecondPage} />
<Route exact path="/" component={HomePage} />
<Route path="*" component={NotFound} />
</Switch>
Reorder like this, it will start working.
Simple :)

React Router 4 Nesting Procedures

I'm going to start by linking the research I've done.
Nested routes with react router v4 - Question
React Router 4 Documentation
Why can I not nest Route components in react-router 4.x? - Question
How to nest routes in React Router v4? - Question
Getting Started with React Router v4 - The Meteor Chef
A Simple React Router v4 Tutorial
Warning: You should not use and in the same route; <Route children> will be ignored - teamtreehouse
React-router-dom v4 nested routes not working - Question
I'm sorry it looks like I can only post 2 external links.
TL;DR
I've found 2 ways to do the nesting in React Router 4, each one has it's advantages and drawbacks.
First. It's react-router's team recommended way, the advantage is that the Route components are where they load, but I find it hard to keep track of the routing.
Second. Manages somehow to do all the routing on one place, but there is some duplicated code and an unnecessary level of nesting by adding a helper component, also I'm not sure if it's ok to do it this way.
So these are the two ways I found to do the nesting. I'm looking for:
Any other ways anyone found to do route nesting.
How should I my nesting? Which way do you think is best?
From this research I've found two ways to do the nesting, I'm working with react-router-dom.
1. React Router Docs Recommended Way
So according to React Router, doing all your routing on the same file is over, so your nesting now should be done by putting our nested routes inside the component.
import React from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
const Home = () => (
<div>
<h2>Home</h2>
</div>
)
const About = () => (
<div>
<h2>About</h2>
</div>
)
const Topics = () => (
<div>
<Route path="/topics/topic" component={Topic}/>
</div>
)
const Topic = () => (
<div>
<h2>One Topic</h2>
</div>
)
const BasicExample = () => (
<Router>
<div>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/topics" component={Topics}/>
</div>
</Router>
)
export default BasicExample
2. Putting it all in one place.
Looking around, people have found ways to do all the routing in one file, like on one of the links I refer to, but it has some drawbacks, for example, to use a "Not Found" page you need to user the Switch component, which is fine, but then if you nest you run into some problems like having to duplicate code. For example, this would work.
2.1 First Level Nesting
const MainLayout = ( {children} ) => (
<div>
<h2>Main Layout</h2>
{children}
</div>
);
const Home = () => (
<div>
<h2>Home</h2>
</div>
);
const About = () => (
<div>
<h2>About</h2>
</div>
);
const FirstLevelNesting = () => (
<Router>
<MainLayout>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route component={NoMatch}/>
</Switch>
</MainLayout>
</div>
</Router>
);
export default FirstLevelNesting;
2.2 Second Level Nesting
Here you can see how to use a helper component to do the nesting on a second level, you can't do the nesting like on the first level, by putting a component like MainLayout inside the Switch because when the Switch reaches it, it will always match the path and we'll never get to NotFound, that's why we need to use a helper component to do the nesting, then again inside that component we also have to add a NotFound.
const NestedRoutes = () => (
<div>
<h2>This is my next nest</h2>
<Switch>
<Route exact path='/nextnest' component={Nest}/>
<Route path='/nextnest/about' component={NestAbout}/>
<Route component={NoMatch}/>
</Switch>
</div>
)
const SecondLevelNesting = () => (
<Router>
<MainLayout>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/nextnest" component={NestedRoutes}
<Route component={NoMatch}/>
</Switch>
</MainLayout>
</div>
</Router>
);

React Router - how to constrain params in route matching?

I don't really get how to constrain params with, for example a regex.
How to differentiate these two routes?
<Router>
<Route path="/:alpha_index" component={Child1} />
<Route path="/:numeric_index" component={Child2} />
</Router>
And prevent "/123" from firing the first route?
React-router v4 now allows you to use regexes to match params -- https://reacttraining.com/react-router/web/api/Route/path-string
const NumberRoute = () => <div>Number Route</div>;
const StringRoute = () => <div>String Route</div>;
<Router>
<Switch>
<Route exact path="/foo/:id(\\d+)" component={NumberRoute}/>
<Route exact path="/foo/:path(\\w+)" component={StringRoute}/>
</Switch>
</Router>
More info:
https://github.com/pillarjs/path-to-regexp/tree/v1.7.0#custom-match-parameters
I'm not sure if this is possible with React router at the moment. However there's a simple solution to your problem. Just do the int/alpha check in another component, like this:
<Router>
<Route path="/:index" component={Child0} />
</Router>
const Child0 = (props) => {
let n = props.params.index;
if (!isNumeric(n)) {
return <Child1 />;
} else {
return <Child2 />;
}
}
* Note that the code above does not run, it's just there to show what I mean.

Resources