React router route loader not working on nested components - reactjs

Am using react router v6 and i would like to use the new loader to load the data before the component loads. So i have the following
In my index.js
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="*"
loader={async ({ params }) => {
console.log("index loader log"); //this logs
return true;
}}
element={<App />}
> </Route>
)
);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<RouterProvider router={router} />
);
in my app component i have nested routes like
const App = () => {
return (
<>
<Routes>
<Route path="auth/*" element={<AuthLayout/>}/>
<Route path="about"
loader={async ({ params }) => {
console.log("about child loader log"); //this doesnt log
return true;
}}
element={<AboutPage/>}/>
</Routes>
<h1>Testing app</h1>
</>
);
}
On the app component the loader on the Route path="about" does not console.log when i visit the about route but the component is rendered. What am i missing for the loader to work on the child route.

Based on some basic testing it seems that in order for the new RRDv6.4 data APIs to work you need to specify the complete routing configuration in the createBrowserRouter function.
There does however appear to already be an issue filed with #remix-run/react-router for this behavior as a reported bug, so you may want to follow it if it ever addressed/resolved. (I suspect it was you since the name is "geoffrey" and the timing is coincidentally about an hour ago around the same time as this post)
This above issue has since been closed with comment:
Descendant <Routes> trees do not participate in data loading
(https://reactrouter.com/en/main/components/routes) since they cannot
be known ahead of render-time. You'll need to lift your descendant
route definitions up into the routes you pass to createBrowserRouter.
The relevant information regarding the descendent routes and the new Data API can be found in the Routes documentation in a note.
Note:
If you're using a data router like createBrowserRouter it is
uncommon to use this component as it does not participate in data
loading.
Hoist the entire route declaration to the parent creating the data router. The following does work with the loader function for the "/about" route and About component.
const router = createBrowserRouter(
createRoutesFromElements(
<Route
path="*"
loader={({ params }) => {
console.log("index loader log");
return "This is the App";
}}
element={<App />}
>
<Route path="auth/*" element={<AuthLayout />} />
<Route
path="about"
loader={({ params }) => {
console.log("about child loader log");
return "this is the about page";
}}
element={<AboutPage />}
/>
</Route>
)
);
The App component should render an Outlet for the nested routes to render their content into.
import { Outlet } from 'react-router-dom';
const App = () => {
return (
<>
<h1>Testing app</h1>
<Outlet />
</>
);
};

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

Creating wrapper component for react-router-dom v6 Route that logs route changes

We are looking to add custom logging to our react application, and would like to log each time a user changes routes. To handle this, we are creating a wrapper component , and this is what we currently have:
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
function LoggerRoute(isExact, path, element) {
useEffect(() => {
// send route-change log event to our mongodb collection
}, []);
// And return Route
return (
isExact
? <Route exact path={path} element={element} />
: <Route exact path={path} element={element} />
);
}
export default LoggerRoute;
...and in our App.js file, we have changed the routes as such:
// remove this // <Route exact path='/tools/team-scatter' element={<TeamScatterApp />} />
<LoggerRoute isExact={true} path='/tools/team-scatter' element={<TeamScatterApp />} />
However, this throws the error Uncaught Error: [LoggerRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>.
Additionally, it feels off passing props to a route, if possible we would prefer
<LoggerRoute exact path='/tools/team-scatter' element={<TeamScatterApp />} />
as the ideal way to call our LoggerRoute. We like the idea of a wrapper component, this way we don’t have to add logging into every component that our app routes to. However, I’m not sure if this wrapper component approach is possible if can only accept a component. How can we modify our LoggerRoute component to work / be better?
In react-router-dom#6 only Route and React.Fragment are valid children of the Routes component. Create either a wrapper component or a layout route component to handle listening for route path changes. Since you presumably want to do this for more than one route at-a-time I suggest the layout route method, but you can create a component that handles either.
Example:
import { Outlet, useLocation } from 'react-router-dom';
const RouteListenerLayout = ({ children }) => {
const { pathname } = useLocation();
useEffect(() => {
// send route-change log event to our mongodb collection
}, [pathname]);
return children ?? <Outlet />;
};
The children prop is used in the cases where you want to wrap individual routed components (i.e. the element prop) or an entire component, (*i.e. App) that is rendering a set of Routes. The Outlet component is used in the case where you want to conditionally include a subset of routes within a Routes component (i.e. some nested Route components).
Wrap the routes you want to listen to route changes for.
Examples:
<Routes>
<Route element={<RouteListenerLayout />}>
<Route path="path1" element={<SomeComponent />} />
<Route path="someOtherPath" element={<SomeOtherComponent />} />
... wrapped routes components with listener
</Route>
... routes w/o listener
</Routes>
or
<Routes>
<Route
path="/"
element={(
<RouteListenerLayout>
<SomeComponent />
</RouteListenerLayout>
)}
/>
</Routes>
or
<RouteListenerLayout>
<App />
</RouteListenerLayout>
These all assume the router is rendered higher in the ReactTree than RouteListenerLayout so the useLocation hook works as expected.
In v6, <Route> is a lot more strict than it was in v5. Instead of building wrappers for , it may be used only inside other <Routes> or <Route> elements. If you try to wrap a <Route> in another component it will never render.
What you should be doing instead is adding a wrapper component and leveraging it in the element prop on the route.
function App() {
return (
<Routes>
<Route path="/public" element={<PublicPage />} />
<Route
path="/route"
element={
<AddLogging>
<YourPage/>
</AddLogging>
}
/>
</Routes>
);
}
Edit: here is an example wrapper component based on your needs:
function AddLogging({children}) {
useEffect(() => {
// send route-change log event to our mongodb collection
// can use useLocation hook to get route to log
}, []);
return children;
}

Adding MsalAuthenticationTemplate for some routes causes all routes to require login

I’ve got a ReactJS website in which I am trying to use "#azure/msal-react": "^1.0.0-beta.1", and ran into some issues that have me flummoxed.
Most of my pages are open to the public. Some require login. If I add the MSALAuthenticationTemplate as below (but with interactionType=Redirect), as soon as I start the site, it asks me to login. I thought it would only do that if I hit a route that was in the AuthenticationTemplate.
Using InteractionType Popup causes the SPA to throw an exception on startup
Error: Objects are not valid as a React child (found: object with keys {login, result, error}). If you meant to render a collection of children, use an array instead. in p (at App.tsx:44)
All of my routes are, for some reason, coming back to the home page instead of loading the relevant components, event with the AuthenticationTemplate commented out.
I had this pretty much working using straight Javascript, but was running into ESLint issues when publishing, so I thought Typescript would help me fix those. But now it’s just broke.
render() {
initializeIcons();
return (
<MsalProvider instance={msalClient} >
<div className="d-flex flex-column h-100">
<TopMenu />
<div className="container-fluid flex-grow-1 d-flex">
<div className="row flex-fill flex-column flex-sm-row">
<BrowserRouter>
<MsalAuthenticationTemplate
interactionType={InteractionType.Popup}
errorComponent={this.ErrorComponent}
loadingComponent={this.LoadingComponent}>
<Switch>
<Route path="/addevent">
<AddEvent />
</Route>
<Route path="/mydashboard">
<MyDashboard />
</Route>
</Switch>
</MsalAuthenticationTemplate >
<UnauthenticatedTemplate>
<Switch>
<Route path='/'>
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/contactus">
<ContactUs />
</Route>
<Route path="/faq">
<Faq />
</Route>
<Route path="/fetchevents">
<FetchEvents />
</Route>
<Route path="/gettingstarted">
<GettingStarted />
</Route>
<Route path="/partners">
<Partners />
</Route>
<Route path="/privacypolicy">
<PrivacyPolicy />
</Route>
<Route path="/sponsors">
<Sponsors />
</Route>
<Route path="/termsofservice">
<TermsOfService />
</Route>
<Route path="/userstories">
<UserStories />
</Route>
</Switch>
</UnauthenticatedTemplate>
<div>
<Footer />
</div>
</BrowserRouter>
</div>
</div>
</div>
</MsalProvider>
);
Let's start with the UnauthenticatedTemplate. If the user is authenticated, children of the component will not show. So I guess you don't want to use it there. It's typical usage is for Login/Logout button for example.
Another problem is that if you are using MsalAuthenticationTemplate as the parent of the Switch and Route components. The problem is that you are guarding switch and routes from unauthenticated users, but this components should always be available without authentication, if you don't want to protect whole page.
During rendering React will go through your components one by one and first child of the BrowserRouter component it will try to render is MsalAuthenticationTemplate and since user is not authenticated, it will redirect user to login page.
This is quote from react-router docs:
A Route is always technically “rendered” even though it’s rendering null. When the 's path matches the current URL, it renders its children (your component).
Because of this the children of the route will only be rendered if the route will be hit. So you need to put MsalAuthenticationTemplate component as a direct child of the route, or even inside such component:
<Switch>
<Route path="/addevent">
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={loginRequest}
>
<AddEvent />
</MsalAuthenticationTemplate>
</Route>
...
</Switch>
As for all the webpages redirected to your home screen, you should add exact keyword to your Home route. This attribute causes it to not match all other routes also. Single '/' matches all your other routes.
In addition to the answer already provided, there is a way (cleaner in my opinion) you can configure MSAL react to take advantage of the router's navigate functions when MSAL redirects between pages in your app.
Here is how it works:
In your index.js file you can have something like so:
import { PublicClientApplication, EventType } from "#azure/msal-browser";
import { msalConfig } from "./authConfig";
export const msalInstance = new PublicClientApplication(msalConfig);
ReactDOM.render(
<React.StrictMode>
<Router>
<ThemeProvider theme={theme}>
<App pca={msalInstance} />
</ThemeProvider>
</Router>
</React.StrictMode>,
document.getElementById('root')
);
As shown, you need to pass msal instance as props to your main App.
Then in your App.js where you setup your routes, you will need to do the following:
import { MsalProvider } from "#azure/msal-react";
import { CustomNavigationClient } from "NavigationClient";
import { useHistory } from "react-router-dom";
function App({ pca }) {
// The 3 lines of code below allow you to configure MSAL to take advantage of the router's navigate functions when MSAL redirects between pages in your app
const history = useHistory();
const navigationClient = new CustomNavigationClient(history);
pca.setNavigationClient(navigationClient);
return (
<MsalProvider instance={pca}>
<Grid container justify="center">
<Pages />
</Grid>
</MsalProvider>
);
}
function Pages() {
return (
<Switch>
<Route path="/addevent">
<AddEvent />
</Route>
<Route path="/mydashboard">
<MyDashboard />
</Route>
<Route path='/'>
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/contactus">
<ContactUs />
</Route>
<Route path="/faq">
<Faq />
</Route>
// your other routes
</Switch>
)
}
And here is the helper function used in App.js that enables navigation by overriding the the default function used by MSAL
import { NavigationClient } from "#azure/msal-browser";
/**
* This is an example for overriding the default function MSAL uses to navigate to other urls in your webpage
*/
export class CustomNavigationClient extends NavigationClient{
constructor(history) {
super();
this.history = history;
}
/**
* Navigates to other pages within the same web application
* You can use the useHistory hook provided by react-router-dom to take advantage of client-side routing
* #param url
* #param options
*/
async navigateInternal(url, options) {
const relativePath = url.replace(window.location.origin, '');
if (options.noHistory) {
this.history.replace(relativePath);
} else {
this.history.push(relativePath);
}
return false;
}
}
You can then use AuthenticatedTemplate on your private pages and UnauthenticatedTemplate on the public pages. For example if you have have addEvent.js (private) and Home.js (public), you will have each components like so:
export function Home() {
return (
<>
<AuthenticatedTemplate>
<p>Welcome Home - it's public</p>
</AuthenticatedTemplate>
</>
);
}
export function AddEvent() {
return (
<>
<UnauthenticatedTemplate>
<Typography variant="h6">
Add event - it is a private page
</Typography>
</UnauthenticatedTemplate>
</>
);
}
Here is a complete example on how to use react-router with msal react for your reference.

How to programatically go one level up of the route hierarchy with React router 5?

I have one functional requirement in my project that it should have a back button which will navigate user to one level up of the route hierarchy.
Consider I have following routes:
// Home Component
<div>
<Route path="/courses" component={CourseComponent} />
<Route path="/settings" component={SettingsComponent} />
</div>
Nested routes:
// Course Component
<Switch>
<Route path={`${match.path}/:courseId/details`} component={CourseDetailComponent} />
<Route path={`${match.path}/classes`} component={ClassComponent} />
<Route
exact
path={match.path}
render={() => <h3>Courses...</h3>}
/>
</Switch>
Now I want to navigate user from CourseDetailComponent to CourseComponent using that back button.
Note: I can't use history since this will use browser history considering the case: if user directly land on a page using a URL.
You have to use one component in your App, which knows all the changes. So, you can have a parent App component, which will be notified from all changes
<Route component={App}>
{/* ... other routes */
</Route>
var App = React.createClass({
getInitialState () {
return { showBackButton: false }
},
componentWillReceiveProps(nextProps) {
var routeChanged = nextProps.location !== this.props.location
this.setState({ showBackButton: routeChanged })
}
})
Reference here: https://github.com/ReactTraining/react-router/issues/1066#issuecomment-139233328

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

Resources