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

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

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

How can I put a route like "/items?Search="?

I have been assigned a task where I must put a route with the following url: /items?search= to load SearchBarScreen component.
I have this code and I am using react-router-dom package:
export const AppRouter = () => {
return (
<Router>
<Switch>
<Route exact path="/items?search=" component={SearchBarScreen} />
<Route exact path="/product/:id" component={ProductDetailScreen} />
<Route path="/" component={HomeScreen} />
<Route component={NotFoundScreen} />
</Switch>
</Router>
);
I can't load my SearchBarScreen component with the url:
http://localhost:3000/items?search=XDDD
the route HomeScreen (/) is loaded in this case :(
what am I doing wrong?
You should feed the URI without the query string to the Route component. So if you want to render your SearchBarScreen at this URL http://localhost:3000/items?search=, you should do this:
<Route exact path="/items" component={SearchBarScreen} />
And then access the query string inside SearchBarScreen like so:
const SearchBarScreen = ({ match, location }) => {
console.log(location.search);
return <div>Search results</div>;
};
Another way is to drop the exact prop.
My recommended solution is the former. Here is a Sandbox: https://codesandbox.io/s/elated-http-lkqfj?file=/src/App.js
The exact param is used when you have multiple paths that have similar names:
for exmplae in your code when you go to //localhost:3000/items?search=XDDD the router will go through all of our defined routes and return the FIRST match it finds.And this is the first thing it finds //localhost:3000/ And executes the HomeScreen component.
The exact param disables the partial matching for a route and makes sure that it only returns the route if the path is an EXACT match to the current url.
So in this case,you should add exact to / route:
<Router>
<Switch>
<Route path="/items?search=" component={SearchBarScreen} />
<Route path="/product/:id" component={ProductDetailScreen} />
<Route exact path="/" component={HomeScreen} />
<Route component={NotFoundScreen} />
</Switch>
</Router>
You can do something like this
You can use this custom hook
useLocation hook is imported from react-router-dom
useQuery() {
return new URLSearchParams(useLocation().search);
}
Then in component
const query = useQuery();
Docs Query param example

Passing props from higher level component to lower level component, React Router in play

I currently have all the Routes in my app defined in App.js. Would like to be able to pass state (as props) from the Alignment component down to the GPfSOA component.
function App() {
return (
<Router>
<div className="App">
<Nav />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
<Route path="/alignments" exact component={Alignments} />
<Route path="/alignments/:id" exact component={Alignment} />
<Route path="/alignments/segmentinfo/:id" exact component={Segments} />
<Route path="/alignments/segmentinfo/:id/:segid" exact component={Segment} />
<Route path="/alignments/getpoint/:id" exact component={GPfSOA} />
<Route path="/alignments/getstatoff/:id" exact component={GSOfPA} />
<Route path="/alignments/getalsfromxy/:x/:y" exact component={AlignList} />
<Route path="/alignments/getsegsfromxy/:x/:y" exact component={SegmentList} />
<Route path="/alignments/post/create" exact component={AddAlignment} />
<Route path="/alignments/put/update/:id" exact component={EditAlignment} />
<Route path="/alignments/ptso/list" exact component={TogglePoints} />
<Route path="/alignments/ptso/list/:ptid" exact component={Point} />
<Route path="/" render={() => <div>404</div>} />
</Switch>
</div>
</Router>
);
}
The order from parent on down to the greatest grandchild would be App > Alignments > Alignment > GPfSOA. Trying to pass item.alignment (the alignment's name) from the Alignment component down (or over) to the GPfSOA component so that it can be rendered there. item.alignment is a property of the Alignmnet component's state.
Do I need to set these up as nested routes in order to accomplish this (a.k.a. cut and paste all the Routes from App.js that are children of the Alignment component and paste them into the Alignment component)?
Having a hard time understanding how to define a particular component as being a parent and another component as being a child of that component. All the examples I see assume you want to pass props from App.js down to some other component. Looking for examples with React Hooks and React Router in play (functions rather than classes) where you're passing props from a component 'below' App.js down to another component that's further down in the hierarchy. Hope this makes sense.
Found lots of examples such as this one for 'passing function as a render props in Route component' (supposedly the recommended way to do this)
const PropsPage = () => {
return (
<h3>Props Page</h3>
);
};
const App = () => {
return (
<section className="App">
<Router>
...
<Link to="/404-not-found">404</Link>
<Link to="/props-through-render">Props through render</Link>
<Switch>
...
<Route exact path="/props-through-render" render={(props) => <PropsPage {...props} title={`Props through render`} />} />
<Route component={NoMatchPage} />
</Switch>
</Router>
about with browser reload
</section>
);
};
export default App;
But like I stated before, this example and every other one I've found assume you want to pass props from App.js down to another component.
Your issue can be handle with creating Alignment context
import React, { createContext, useState } from "react";
const AlignmentContext = createContext();
const AlignmentContextProvider = ({ children }) => {
const [num, setNum] = useState(1);
};
return (
<AlignmentContext.Provider value={{ num, setNum }}>
{children}
</AlignmentContext.Provider>
);
};
export { AlignmentContext, AlignmentContextProvider };
now wrap your routes needed to be in same context with AlignmentContextProvider
import { AlignmentContextProvider } from 'pathto/context'
<AlignmentContextProvider>
<Route path="/alignments/:id" exact component={Alignment} />
<Route path="/alignments/segmentinfo/:id" exact component={Segments} />
<Route path="/alignments/segmentinfo/:id/:segid" exact component={Segment} />
<Route path="/alignments/getpoint/:id" exact component={GPfSOA} />
</AlignmentContextProvider>
and use useContext hooks for reach values
import React, { useContext } from "react";
import { AlignmentContext } from 'pathto/context';
const GPfSOA = () => {
const { num, setNum } = useContext(AlignmentContext);

React Router v4 dynamic nested routes do not match

I ask this question just want to make sure that I understand the dynamic nested routes in React Router v4 correctly. I want to build a hacker news client similar to this one. But I am bogged down by setting up the dynamic routes.
In React Router v4, if I follow other tutorials on the web using the match object I will have something like this (A super simple one):
const ChildComponent = ({rows, match}) => (
<div>
<ol>
rows.map((row, i) => {
return (
<li key={row.id}>
<a href={row.url}>row.title</a> // topic title
<Link to=`${match.url}/${row.by}`>row.by</Link> // topic author
</li>
)
}
</ol>
<Route path=`${match.url}/:userId` render={(props) => <UserProfile fetchUserData={fetchUserData} {...props} />} />
</div>
)};
And when we render the parent component, we usually use something like this for routing:
<Switch>
<Route path="/" render={Home} />
<Route path="/topics" render={(props) => <ChildComponent rows={rows} {...props} /> } />
<Route path="*" render={() => <div>Not Found</div>} />
</Switch>
But this is not ideal for this case, as when I click to view the author's info I need to display a url like this: "http://mysite/user/userid" instead of the current one which is "http://mysite/news/userid".
However, if I change ${match.url}/${row.by} to /user/${row.by} and change ${match.url}/:userId to /user/:userId the route is not recognized in the app. The route begins with /user/ is simply skipped, it will go straight to the app's NotFound route (if there is one), which is in the parent component. Why will the links in child component try to match the routes in the parent if I don't use ${match.url} in the route?
I have added a demo for you to easier to understand this problem.
Because when you'll click the /user/:userId: link the app will parse the Router's Switch to see if something matches. If it doesn't it fallback to *.
In your case, you did not specify anything in the Switch to handle /user .
You'll need to move your userId Route declaration to the Switch as they won't share the same first route (/user !== /topics).
<Switch>
<Route path="/" render={Home} />
<Route path="/topics" render={(props) => <ChildComponent rows={rows} {...props} /> } />
<Route path="/user/:userId" render={(props) => <UserProfile fetchUserData={fetchUserData} {...props} />} />
<Route render={() => <div>Not Found</div>} />
</Switch>

React-Router v4 shared layout re-renders

I know that this question has been asked before, but I keep having issues with this.
The issue I have is that when I use a Page Layout-like component to wrap my routes, this page layout is re-rendered when changing path.
In react-router v3 I did something like this:
<Router history={this.props.history}>
<Route path="/">
<IndexRedirect to="/Dossiers" />
<Route path="/Dossiers" component={MainLayout}>
<IndexRoute component={DossiersPage} />
<Route path="/Dossiers/:dossierId/:title" component={DossierDetailsPage} />
</Route>
</Route>
</Router>
When moving paths, this would NOT re-render the MainLayout component (which is easily checked by putting something in state inside MainLayout).
Now, in react-router v4 I tried a couple of approaches already:
Wrapping Switch with the MainLayout component
Creating a RouteWithMainLayout component which wraps Route (as described here: https://simonsmith.io/reusing-layouts-in-react-router-4/)
Some of the approaches described here: https://github.com/ReactTraining/react-router/issues/3928
However, all solutions I've tried seem to re-render the MainLayout component, basically causing state to reset to its initial value(s).
tldr; How do I create a wrapping component in react-router v4 which doesn't re-render when changing paths
I put together a codesandbox example of how I'm using a "page layout" type of component. It uses React Router v4.1.2.
https://codesandbox.io/s/Vmpy1RzE1
As you described in your question, and as was described in Matt's answer, the MainLayout component wraps the routes.
<BrowserRouter>
<MainLayout>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
</Switch>
</MainLayout>
</BrowserRouter>
It is true that the MainLayout component re-renders when I navigate the app, in the sense that render is called by React. But, the MainLayout component is never unmounted, so the state never re-initializes.
I've placed some console.logs around my example to show this. My MainLayout looks like this:
export default class MainLayout extends React.Component {
state = {
layoutCreatedOn: Date(),
};
componentDidMount() {
//This will not fire when we navigate the app.
console.log('layout did mount.');
}
componentWillUnmount() {
//This won't fire,
// because our "shared page layout" doesn't unmount.
console.log('layout will unmount');
}
render() {
//This does fire every time we navigate the app.
// But it does not re-initialize the state.
console.log('layout was rendered');
return (
<div styles={styles}>
<h5>
Layout created: {this.state.layoutCreatedOn}
</h5>
<Sidebar />
{this.props.children}
</div>
);
}
}
As you click around the app, you'll see a few things.
componentDidMount fires only once.
componentWillUnmount never fires.
render fires every time you navigate.
Despite this, my layoutCreatedOn property shows the same time as I navigate the app. The state is initialized when the page loads, and never re-initialized.
You no longer need IndexRedirect, instead just wrap all of your routes in your MainLayout component, such as:
<Router history={this.props.history}>
<Switch>
<MainLayout>
<Route path="/" component={DossiersPage}/>
<Route path="/Dossiers/:dossierId/:title" component={DossierDetailsPage} />
</MainLayout>
</Switch>
</Router>
Here is the correct solution for React Router v4 as stated here
So basically you need to use the render method to render the layout and wrap your component like this:
<Router>
<Switch>
<Route path={ROUTES.LOGIN} render={props =>
<LoginLayout {...props}>
<Login {...props} />
</LoginLayout>
} />
<Route path={ROUTES.REGISTER} render={props =>
<LoginLayout {...props}>
<Register {...props} />
</LoginLayout>
} />
<Route path="*" component={NotFound} />
</Switch>
</Router>
This will not cause re-rendering of the layout when you are changing the routes.
When you have many different components with many different layouts you can go ahead and define them in a route config array like the example from the issue I linked:
const routes = [
{ path: '/',
exact: true,
component: Home
},
{ path: '/about',
component: About,
},
{ path: '/cart',
component: Three,
}
]
<Router>
<Switch>
{routes.map({ path, exact, component: Comp } => (
<Route path={path} exact={exact} render={(props) => (
<LayoutWithSidebarAndHeader {...props}>
<Comp {...props}/>
</LayoutWithSidebarAndHeader>
)}/>
))}
<Route component={Error404}/>
</Switch>
</Router>

Resources