React router - code splitting lazy imports. Switch with Suspense outside - reactjs

I'm using module federation from webpack and my core app contained all routes to the rest of the app. What works fine is that inside the Switch, I just had each AuthRoute or Route manually rather than using the map. Suspense was wrapping the Switch so that the direct children are just Route. I'm now doing some splitting but I can't get it to work. Any ideas?
my routes are set up as so (and localRoutes is at the bottom):
const routes = [
...localRoutes,
// ...remoteRoutes
];
Inside my BrowserRouter I map routes based on whether the user is authorised for that route or not. I suspect the problem is here but don't understand why Route or AuthRoute that returns a Route won't work since it's a directly under the Switch.
<Switch>
{routes.map((route) => {
console.log(route)
route.auth ?
<AuthRoute
key={route.path}
path={route.path}
component={route.component}
exact={route.exact}
requiredRoles={route.requiredRoles}
/>
:
<Route
key={route.path}
path={route.path}
component={route.component}
exact={route.exact}
/>
})}
<Redirect to='/login' />
</Switch>
where authRoute:
const AuthRoute = ({ Component, path, exact, requiredRoles }) => {
const isLoggedIn = true // or false
const roles = ['admin', 'sth_else']
const userHasRequiredRole = intersection(requiredRoles, roles).length > 0
const message = userHasRequiredRole ? 'Please log in to view this page' : "You can't be here!"
return (
<Route
exact={exact}
path={path}
render={(props) =>
isLoggedIn && userHasRequiredRole
? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: userHasRequiredRole ?
'/login' :
'/modules',
state: {
message,
requestedPath: path
}
}}
/>
)
}
/>
);
};
export default AuthRoute;
and example routes:
const AboutPage = lazy(() => import('core/AboutPage'))
const LoginPage = lazy(() => import('core/LoginPage'))
const MyModules = lazy(() => import('core/MyModules'))
const routes = [
{
auth: true,
path: "/modules",
component: MyModules,
exact: false,
requiredRoles: [
String(UserRoles.Administrator),
String(UserRoles.AnotherRole),
String(UserRoles.Another)
]
},
{
auth: false,
path: "/about",
component: AboutPage,
exact: false,
}
];

If you are doing lazyLoad specify component as a function inside <Route>.
In your case try the following:
<Route
key={route.path}
path={route.path}
component={(props) => (<route.component {...props} />)}
exact={route.exact}
/>

I think the above code should work if you add a return before ternary operator
retur route.auth ?...
I am having similar structure in my app
<Suspense fallback={<Spinner />}>
<Switch>
<Refresh path="refresh" />
{routes.map((route) => {
return <PrivateRoute key={route.path} {...route} user={user} />;
})}
{routes.length && user && <Route component={NotFound} />}
</Switch>
</Suspense>

Related

React-Router-Dom 6 - How to dynamically render a component?

My old method:
<Route
key={i}
path={path}
render={(props) => {
if (!localStorage.getItem("token")) {
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
}
return (
<AuthLayout>
<Component {...props} />
</AuthLayout>
);
}}
/>
Replacing render with the new element gives me:
Functions are not valid as a React child. This may happen if you return a Component instead of from render
Apparently the new API simply expects:
<Route
key={i}
path={path}
element={
<Component />
}
/>
What I'm really trying to accomplish is to dynamically render the component as such:
{authProtectedRoutes.map(({ path, Component }, i) => {
<Route
key={i}
path={path}
element={
// If no auth token, redirect to login
if (!token) {
<Navigate to="/login" />
} else {
<AuthLayout>
<Component />
</AuthLayout>
}
}
/>
})}
Not sure how to do this ...
EDIT:
My array of components is as such:
const authProtectedRoutes = [
{ path: "/dashboard", Component: Dashboard },
{ path: "/pages-starter", Component: StarterPage },
When I try to return Component in my loop I get:
React.jsx: type is invalid -- expected a string (for built-in
components) or a class/function (for composite components) but got:
undefined. You likely forgot to export your component from the file
it's defined in, or you might have mixed up default and named imports.
element={
// If no auth token, redirect to login
if (!token) {
<Navigate to="/login" />
} else {
<AuthLayout>
<Component />
</AuthLayout>
}
}
You can't do an if in the middle of jsx, but you can do a conditional operator:
element={!token ? (
<Navigate to="/login" />
) : (
<AuthLayout>
<Component />
</AuthLayout>
)}
The element prop expects a ReactNode (a.k.a. JSX) and not javascript (i.e. the if-statement).
Since it seems you render your authenticated routes in bulk a more optimal solution would be to wrap them all in a single AuthLayout component that checks the token. Instead of rendering the children prop it renders an Outlet for nested routes to be rendered into.
Example:
const AuthLayout = ({ token }) => {
// ... existing AuthLayout logic
return token
? (
<div /* awesome auth layout CSS style */>
...
<Outlet /> // <-- nested routes render here
</div>
)
: <Navigate to="/login" />;
};
Don't forget to return the Route from the map callback.
<Route element={<AuthLayout token={token} />}>
{authProtectedRoutes.map(({ path, Component }) => (
<Route key={path} path={path} element={<Component />} />
))}
</Route>
Nice routing-related question. First of all, I found useful code example from react-router-dom's github: https://github.com/remix-run/react-router/blob/2cd8266765925f8e4651d7caf42ebe60ec8e163a/examples/auth/src/App.tsx#L104
Here, instead of putting some logics inside "element" or "render" authors suggest to implement additional RequireAuth component and use it in routing setup like following:
<Route
path="/protected"
element={
<RequireAuth>
<SomePageComponent />
</RequireAuth>
}
....
This approach would allow to incapsulate auth-related checks inside this new RequireAuth component and as a result make your application routing setup more "lightweight"
As a "brief" example, I created following piece of code you could reference to:
function App() {
return (
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
);
}
const RequireAuth = ({ children }) => {
const token = localStorage.getItem('token');
const currentUrl = useHref();
return !token ? (
<Navigate to={`/login?redirect=${currentUrl}`} />
) : (
children
);
};
const authProtectedRoutes = [
{ path: '/', component: PaymentsPage },
{ path: '/user', component: UserInfoPage },
];
const AppRoutes = () => (
<Routes>
{authProtectedRoutes.map((r) => (
<Route
key={r.path}
path={r.path}
element={
<RequireAuth>
<AuthLayout>
<r.component />
</AuthLayout>
</RequireAuth>
}
/>
))}
<Route path="/login" element={<LoginPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
);

Login redirection not working with react router

I'm working on an app with login and private routes, so far I have done the login and declared the private routes like this...
<Route path="/" render={() => <Redirect to="/app" />} />
<PrivateRoutes path="/app">
<AdminLayout>
<Switch>
<Route path="/app" exact render={() => <Redirect to="/app/dashboard" />} />
<Route path="/app/dashboard" component={Dashboard} />
</Switch>
</AdminLayout>
</PrivateRoutes>
<HomeLayout>
<Switch>
<Route path="/login" component={LogInPage} />
</Switch>
</HomeLayout>
</BrowserRouter>
then i create my private routes:
!!isAuthenticated ? (
<Route {...rest} render={() => children} />
) : (
<Redirect
to={{
pathname: '/login',
state: { from: history.location },
}}
/>
);
finally, i redirect to app/dashboard
if (isLogged) {
return <Redirect to="/app/dashboard" />;
}
return (
<form onSubmit={handleSubmit}>
so what is happening login works ok, it changes the URL but I don't see the components of the private route, I debugged it and it never declares these routes, if I keep the token and refresh I'm redirected as intended.
On the other hand a side question, im using hooks, how can I see that state: { from: history.location }, on the login component?
As Drew Reese commented my component was not updating this is why:
const PrivateRoutes = ({ children, ...rest }: Props) => {
useInjectReducer({ key: sliceKey, reducer: reducer });
useInjectSaga({ key: sliceKey, saga: authSaga });
const dispatch = useDispatch();
const history = useHistory();
const isAuthentificating = useSelector(selectIsAuthentificating);
const isAuthenticated = useSelector(selectIsAuthenticated);
useEffect(() => {
const token: string = cookies.get('token') || '';
dispatch(actions.getAccountRequest({ token }));
return () => {
dispatch(actions.reset());
};
}, [dispatch]);
return isAuthentificating ? (
I was getting the token inside the effect and that never happened I took it outside and it worked perfectly
For the other issue the state objected sended in the redirect is in the useLocation() hook

How to configure react-route with private route?

I cannot configure the route correctly. Shop component not renders
Component structure:
index.tsx:
const Index: FC = () => {
const SignIn = props => (
<LazyLoad
component={React.lazy(() => import('./modules/auth/containers/SignIn'))}
{...props}
/>
)
return (
<Provider store={store}>
<EmotionThemeProvider>
<ThemeProvider theme={theme}>
<Router>
<Switch>
<PrivateRoute exact path='/' component={Main} />
<Route path='/signin' component={SignIn} />
</Switch>
</Router>
</ThemeProvider>
</EmotionThemeProvider>
</Provider>
)
}
PrivateRoute.tsx:
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
localStorage.getItem('accessToken') ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: '/signin', state: { from: props.location } }}
/>
)
}
/>
)
Main.tsx:
const Main: FC = () => (
<App>
<Switch>
{routes.map(route => (
<Route
key={route.path}
path={route.path}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
</App>
)
Shop.tsx:
const Shop = () => {
const [, setCollapsed] = useContext(SidebarContext)
useEffect(() => {
setCollapsed(true)
}, [])
return <div>Shop</div>
}
routes.ts:
export const routes = [
{
path: '/shop',
key: 'market',
component: props => (
<LazyLoad
component={React.lazy(() => import('./modules/shop/pages/Shop'))}
{...props}
/>
),
},
{
path: '/',
exact: true,
component: () => <div>main</div>,
skip: true,
},
]
When I logged in, the main page opens, but after when I go to the shop, it no longer renders.
Where is my mistake? I have tried to remove 'exact' from PrivateRoute, then shop opens, but I got this error:
Uncaught Error: Maximum update depth exceeded. This can happen when a
component repeatedly calls setState inside componentWillUpdate or
componentDidUpdate. React limits the number of nested updates to
prevent infinite loops

Routes requiring authentication in react another way to handle (PrivateRoutes)

I'm looking for a way to do some route protection with react-router-4. Looking at an example in the documentation, they create a Component which is rendered like this:
<PrivateRoute path="/protected" component={Protected} />
and the privateRoute component:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)}/>
)
I don't really understand why I should want to pass the "Protected" component as a property and then have to take care of spreading all the ...props and ...rest.
Before I was reading this doc (and other example), I created the following code, which just nest the routes in another component which takes care of the authentication part.
Because my example (which seems to work perfectly well), looks way more simplistic, I must be missing something.
Are there any downsides on this approach?
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import Nav from './Nav';
// dummy
const Auth = {
isAuthenticated: () => { return true; }
}
const Home = () => <h1>Home</h1>
const SignIn = () => <h1>SignIn</h1>
const About = () => <h1>About</h1>
class PrivateOne extends Component {
render() {
console.log(this.props);
return <h1>Private</h1>
}
}
const PrivateTwo = () => <h1>PrivateTwo</h1>
const PrivateThree = () => <h1>PrivateThree</h1>
const NotFound = () => <h1>404</h1>
const Private = ({isAuthenticated, children}) => {
return(
isAuthenticated ? (
<div>
<h1>Private</h1>
{children}
</div>
) : (
<Redirect to={{
pathname: '/sign_in',
}}/>
)
)
}
const App = () =>
<div>
<Router>
<div>
<Nav />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/sign_in" component={SignIn} />
<Private isAuthenticated={Auth.isAuthenticated()}> {/* or some state later on */}
<Route path="/private1" component={PrivateOne} />
<Route path="/private2" component={PrivateTwo} />
<Route path="/private3" component={PrivateThree} />
</Private>
<Route component={NotFound} />
</Switch>
</div>
</Router>
</div>
export default App;

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