How to pass key to react route? - reactjs

I am separating routes based on roles. Each PrivateRoute is a route.
Route.tsx
export default function MCRoutes({ isUserAuthenticated, role }) {
const token = useFetchToken();
// const { data } = useFetchDataFromState("activeModule");
let privateRoutes = mcRoutesConfig[role].routes.map(
({ id, component, path, exact }) => (
<PrivateRoute
// key={id}
exact={exact}
isUserAuthenticated
path={path}
component={component}
/>
)
);
return (
<Switch>
{privateRoutes}
<Route component={NoPageFound} />
</Switch>
);
}
PrivateRoute
export default function PrivateRoute({
component: Component,
isUserAuthenticated,
...props
}) {
return (
<Route
{...props}
render={innerProps => {
return isUserAuthenticated ? (
<PrivateLayout>
<Component {...innerProps} />
</PrivateLayout>
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
);
}}
/>
);
}
But when I do this, React asks me to provide key for each item in map function. And if I pass key as prop then it remounts components when route changes. How to solve this ?

My solution is to use the same key for all routes. No warning and seems like it works just fine.
const Routes = ({ routes }) => {
return (
<Switch>
{routes.map(props => {
return <RouteWithLayout key={1} {...props} />
})}
</Switch>
)
}
This is most probably working due to how Switch works. It'll check paths and renders the only one exactly that matches.

The map function in JS provides a second argument to the function you give it, which is the index of the current iteration. The React community suggests that you can use that if you cannot use other properties.
So you could do like this:
...
let privateRoutes = mcRoutesConfig[role].routes.map(
({ id, component, path, exact }, index) => (
<PrivateRoute
key={index}
exact={exact}
isUserAuthenticated
path={path}
component={component}
/>
)
);
return ...
That should work.

Related

Is it bad approach to make wrappers for public/private routes in react-router v6 like this

In app I know that user is authenticated when there is token in redux.
When app loads I first fetch that token from server.
I made a wrapper for private and public routes.
I made wrapper for public routes because if user is on login page and reload app once token is fetched I need to make redirect to authenticated page.
What troubles me is this approach ok and does it make performance problems for the future?
Without public route wrapper user is able to access login page when authenticated (which should not).
I am trying to make a clean solution as possible.
This is my routes rendering:
return (
<Routes>
{routes.map((route, index) => {
if (route.isProtected) {
return (
<Route
key={index}
path={route.path}
element={
<RequireAuth> <<<< wrapper
<route.component />
</RequireAuth>
}
/>
);
} else {
return (
<Route
key={index}
path={route.path}
element={<Public><route.component /></Public>} <<<< wrapper
/>
);
}
})}
<Route path="*" element={<NotFoundPage />} />
</Routes>
);
Public wrapper
function Public({ children }: { children: JSX.Element }) {
const { state } = useLocation();
const {token, tokenChecking} = useAppSelector((state) => state.auth);
if (tokenChecking) {
return (
<div>app is loading...</div>
)
}
if (token) {
return <Navigate to={state?.from || "/dashboard"} replace/>;
}
return children;
}
Private route is more or less reverse
function RequireAuth({ children }: { children: JSX.Element }) {
let location = useLocation();
const {token, tokenChecking} = useAppSelector((state) => state.auth);
if (token) {
return (
<div>App is loading ...</div>
)
}
if (!token) {
return <Navigate to={"/login"} state={{ from: location }} />;
}
return children;
}
What troubles me is this approach ok and does it make performance
problems for the future?
This approach seems reasonable to me and I don't foresee any performance issues with it.
If you want to make the code a bit more DRY then I suggest looking at the isProtected flag first and instantiate the wrapper first, then render a single wrapper and component. Remember that React components are PascalCased, so create a Component to render into the wrapper.
return (
<Routes>
{routes.map((route) => {
const Wrapper = route.isProtected ? RequireAuth : Public;
const Component = route.component;
return (
<Route
key={route.path}
path={route.path}
element={
<Wrapper>
<Component />
</Wrapper>
}
/>
);
})}
<Route path="*" element={<NotFoundPage />} />
</Routes>
);

React wrapper to return child on <Route> component

Is it possible in React to conditionally render a inside a using this kind of component?
import PropTypes from 'prop-types'
import { useApplication } from 'ice-components'
const canEdit = (currentUser) => {
if (currentUser.settings.canEditSpecRec.value === 'False' || !currentUser.location.isSpecimenReceptionType) {
return false
}
return true
}
const Permissions = ({ children }) => {
const { currentUser } = useApplication()
if (!canEdit(currentUser)) {
return null
}
return children
}
Permissions.propTypes = {
children: PropTypes.node,
}
export { Permissions as default, canEdit }
And the route is made in this way:
<Switch>
<Route exact path component={component_1} />
<Route path='/anotherPath' component={component_2} />
<Switch>
I tried to wrap the Permissions component around a single Route component but it breaks. Any ideas?
A good way to conditionally render a Route is the one provided by the section Auth of the react-router documentation:
https://reacttraining.com/react-router/web/example/auth-workflow
What's different in your application is that you're not authorizing based on authentication but on some user permission, you could do something like this:
function PrivateRoute({ children, ...rest }) {
return (
<Route
{...rest}
render={({ location }) =>
canEdit(currentUser) ? (
children
) : (
<Redirect
to={{
pathname: "/",
}}
/>
)
}
/>
);
}
Wrapping the Route in a PrivateRoute component that will return your route if user has permission to or something else if not (maybe a redirect).
export default function AuthExample() {
return (
<Router>
<div>
<Switch>
<Route path="/public">
<PublicPage />
</Route>
<Route path="/login">
<LoginPage />
</Route>
<PrivateRoute path="/protected">
<ProtectedPage />
</PrivateRoute>
</Switch>
</div>
</Router>
);
}

React Router match property null when using custom Route component

I am using the ConnectedRouter component and a custom Route like so:
const PrivateRoute = ({ layout: Layout, component: Component, rest }) => (
<Route
{...rest}
render={matchProps => (
<AuthConsumer>
{({ authenticated }) =>
authenticated ? (
<Layout pathname={matchProps.location.pathname}>
<Component {...matchProps} />
</Layout>
) : (
<Redirect to={{ pathname: '/login', state: { from: matchProps.location } }} />
)
}
</AuthConsumer>
)}
/>
)
which I use like this:
<Switch>
<PrivateRoute
exact
path={`${match.url}someroute/:id`}
layout={Layout}
component={SomePage}
/>
</Switch>
And a component like so:
import React from 'react'
const SomePage = ({ match }) => {
console.log('MATCH ', match.params)
return <div>TESTING</div>
}
export default SomePage
In this case, match is empty, and it thinks its on the '/' route (even though location prop says its on /someroute/123.
On the other hand, this works:
<Route
exact
path={`${match.url}someroute/:id`}
component={SomePage}
/>
and match gets updated properly.
I'm stumped as to why this is happening. Any help would be appreciated!
Ah, I figure it out. rest should have been ...rest, the <Route> component wasn't getting the props passed down correctly!

Protected Route in React, not passing parameters

I am trying to create a protected route as shown in the code below:
const PrivateRoute = ({component:Component, authenticated, ...rest}) =>{
console.log(authenticated);
return(
<Route {...rest} render={(props) => (
authenticated === true
? <Component {...props} {...rest} />
: <Redirect to="/Login"/>
)} />);
}
export default PrivateRoute;
I am passing the following params in my Router configuration:
<PrivateRoute component={Appointments} authenticated={this.state.authenticated} exact path="/appointments" render={(props) => <Appointments {...props} appointments={this.state.appointments} />} />
.
However, when I try routing, it appears that the "appointments={this.state.appointments}" prop is not getting passed to the "Appointments" component.
This is the error that I get
TypeError: Cannot read property 'map' of undefined
Any idea what the issue could be?
I think the problem here is that you are passing props in the render property of the PrivateRoute which is as render={(props) => <Appointments {...props} appointments={this.state.appointments} />}. Now this property is not being Utilised in your actual component rendering in the PrivateRoute. Try following during your initialisation of route:
<PrivateRoute component={Appointments} authenticated={this.state.authenticated} exact path="/appointments" appointments={this.state.appointments} />
This should fix your issue. I would suggest here that rather than creating a PrivateRoute of your own, you can use a React HOC to create an authentication wrapper on the actual component.
function PrivateRoute({ children, ...rest }) {
const isAuthenticated = (location) => {
//if loading show loading indicator
}
const childrenWithLocation = (location) => Children.map(children, child => {
if (isValidElement(child)) {
return cloneElement(child, { to: child.props?.to + location.search });
}
return child;
});
return (
<Route
{...rest}
render={({ location }) =>
isAuthenticated(location)
? (childrenWithLocation(location))
: (<Redirect
to={{
pathname: "/login" + location.search,
state: { from: location }
}}
/>)
}
/>
);
}

Reload or typing URL manually always redirect to root

The title says it all.
When I type a url manually in a browser or try to refresh, it always goes back to root, is there any thing wrong? because I'm not getting it.
When I refresh or type url manually and I'm logged in it goes to the dashboard, and when I'm not logged in, it goes to login page.
Here is my code
import { Switch, Route, Redirect, Router } from 'react-router-dom';
<Provider store={this.props.store}>
<IntlProvider locale={this.props.locale} messages={this.props.localeData}>
<LocaleProvider locale={enUS}>
<Router history={history}>
<Switch>
<Route exact path="/" render={() => (<Redirect to="/dashboard" />)} />
<PrivateRoute path="/dashboard" component={App} locale={this.props.locale} redirectTo="/login" />
<PropsRoute path="/login" component={Login} />
<PropsRoute path="/reset-password" component={ResetPassword} />
<PropsRoute path="/loader" component={Loader} spinning={true} fullScreen={true} />
<Route component={NoMatch} />
</Switch>
</Router>
</LocaleProvider>
</IntlProvider>
</Provider>
this is my props route
const renderMergedProps = (component, ...rest) => {
const finalProps = Object.assign({}, ...rest);
return (
React.createElement(component, finalProps)
);
};
const PropsRoute = ({ component, ...rest }) => {
return (
<Route {...rest} render={routeProps => {
return renderMergedProps(component, routeProps, rest);
}} />
);
};
and my private route
const PrivateRoute = ({ user, component, redirectTo, ...rest }) => {
return (
<Route {...rest} render={routeProps => {
return user.logged ? (
renderMergedProps(component, routeProps, rest)
) : (
<Redirect to={{
pathname: redirectTo,
state: { from: routeProps.location }
}} />
);
}} />
);
};
Note: I'm also using Redux and Redux Saga.
const PrivateRoute = ({ user, component, redirectTo, ...rest }) => {
return (
<Route {...rest} render={routeProps => {
return user.logged ? (
renderMergedProps(component, routeProps, rest)
) : (
<Redirect to={{
pathname: redirectTo,
state: { from: routeProps.location }
}} />
);
}} />
);
};
Would say you should check if your user.logged hasn't change, it may be the case if your re-rendering the app when you manually change the url.
You could use localstorage to check if user is indeed logged or not.

Resources