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

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>
</>
);
};

Related

Update state variable within Router

I'm currently working on a React site, and I have a custom HeaderBar component, and I would like to display the current page title on it. I had planned to just update a state variable from the Router to change the title. However, understandably, this results in a "too many re-renders" error.
Here's my current code:
function App() {
const [pageTitle, setPageTitle] = useState("")
return (
<div>
<Router>
<HeaderBar siteTitle="My Website Name" pageTitle={pageTitle} />
<Switch>
<Route path="/login">
{setPageTitle("Login")}
<h1>Login Page</h1>
</Route>
<Route path="/main">
{setPageTitle("Main")}
<h1>Main Page</h1>
</Route>
<Route path="/about">
{setPageTitle("About")}
<h1>About Page</h1>
</Route>
</Switch>
</Router>
</div>
);
}
Obviously I could just move the HeaderBar declaration inside each of the Routes, but this seems like a hackish solution to me.
Is there a better way to go about this?
When calling functions inside JSX, these functions are executed on every render, which would results on the 3 setPageTitle calls being executed on every render (although in your case, only the first setPageTitle is going to be called thus resulting in the too many updates as it would be cycling between calling the first setPageTitle, updating the state, rendering again, and calling it once again).
Ideally, if you define components for each route, you would need to call setPageTitle outside of the return statement (meaning outside of the JSX, before rendering), which would result in the setPageTitle being called only when you access the corresponding route.
In order to do that without defining components for your routes, you could do the following:
function App() {
const [pageTitle, setPageTitle] = useState("")
return (
<div>
<Router>
<HeaderBar siteTitle="My Website Name" pageTitle={pageTitle} />
<Switch>
<Route path="/login" render={() => {
setPageTitle("Login");
return (<h1>Login Page</h1>);
}}>
</Route>
<Route path="/main" render={() => {
setPageTitle("Main");
return (<h1>Main Page</h1>);
}}>
</Route>
<Route path="/about" render={() => {
setPageTitle("About");
return (<h1>About Page</h1>);
}}>
</Route>
</Switch>
</Router>
</div>
);
}

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 :)

Layout routes with react router

I'm trying to do layouts with react-router.
When my user hits / I want to render some layout. When my user hits /login, or /sign_up I want the layout to render, with the relevant component for /login or /sign_up rendered.
Currently, my App.js looks like this
return (
<div className={className}>
<Route path="/" component={Auth} />
<ModalContainer />
</div>
);
My Auth.js looks like this
return (
<AuthFrame footerText={footerText} footerClick={footerClick}>
<Route path="/login" component={LoginContainer} />
<Route path="/sign_up" component={SignUpContainer} />
</AuthFrame>
);
So AuthFrame will get rendered when I hit /, and then react router looks for login or sign_up to render the other containers.
However, when I hit /, only the AuthFrame will render.
I would like for / to be treated as /login.
How do I achieve this?
The Switch component is useful in these cases:
return (
<AuthFrame footerText={footerText} footerClick={footerClick}>
<Switch>
<Route path="/login" component={LoginContainer} />
<Route path="/sign_up" component={SignUpContainer} />
{/* Default route in case none within `Switch` were matched so far */}
<Route component={LoginContainer} />
</Switch>
</AuthFrame>
);
see: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Switch.md
I think you're forced to introduce a prop/state which indicates the status of your viewer. This means is he signed in or just a guest of your website.
Your router can't obviously render /login if you you hit / but the router allows you to redirect to another page:
class AuthContainer extends React.Component {
defaultProps = {
loggedIn: false
}
render() {
return <div>
<Route path="/login" component={LoginContainer}/>
<Route path="/sign_up" component={SignUpContainer}/>
</div>
}
}
class PublicHomePage extends React.Component {
render() {
return <div>
<Route path="/settings" component={SettingsComponent}/>
<Route path="/profile" component={ProfileComponent}/>
<Route path="/and_so_on" component={AndSoOnComponent}/>
</div>
}
}
class App
extends React.Component {
defaultProps = {
loggedIn: false
}
render() {
const {loggedIn} = this.props;
if (loggedIn) {
return <PublicHomePage/>
}
return <Route exact path="/" render={() => (
<Redirect to="/login"/>
)}/>
}
}
I hope this code works for you. It isn't quite perfect but it should give you an idea how you could solve your problem.
In your case I would probably manipulate a bit with Routes in react-router. This code in AuthFrame should do the trick:
return (
<AuthFrame footerText={footerText} footerClick={footerClick}>
{["/", "/login"].map((path, ind) =>
<Route exact key={ind} path={path} component={LoginContainer} />
)}
<Route exact path="/sign_up" component={SignUpContainer} />
</AuthFrame>);
Note the usage of exact on the routes, this is to prevent matching login component on /sign_up since it will also match / and prevent rendering both login and signup when accessing the root path (/).

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