I'm taking my first steps with react-router.
I'm currently using the hashHistory for development purposes and I'm performing 'manual' navigation. That is to say, I'm not using Link and I'm invoking history.push('/some/route'); in order to navigate (in response to plain old clicks on anchor tags).
What I'm noticing is that, even when I'm already on the target route, react-router will re-render the relevant target component every time history.push('/target/route'); is invoked: On every push('/target/route'):
the fragment part of the URL remains #/target/route
the query string part of the URL changes to ?_k=somethingRandom
the target component re-renders
I would like for that re-rendering to not happen - I actually expected history.push to be a no-op when I'm already at the route that I'm attempting to push.
I'm apparently missing something, as this is not what's happening. Funnily enough I'm seeing posts from people who are trying to achieve the behaviour that I'd like to get rid of - they'd like to 'refresh' a route without leaving it, so to speak. Which looks pretty much like the opposite problem :).
Could you enlighten me as to what it is I'm misunderstanding and how I would achieve the desired behaviour? Is this perhaps something that would go away if (when) I switch to browserHistory?
My guess is that your component re-renders because something in your prop changes when you make a router push. I suspect it might be the action or key properties of prop.location. You could always check all the values of prop during each render to see what changes.
You can solve this issue by comparing your old route path with the new one in the shouldComponentUpdate life-cycle method. If it hasn't changed you are on the same route, and you can prevent the re-rendering by returning false. In all other cases, return true. By default this always returns true.
shouldComponentUpdate: function(nextProps, nextState) {
if(this.props.route.path == nextProps.route.path) return false;
return true;
}
You'll have to make further checks as well as this will prevent your component from updating on state updates within the component as well, but I guess this would be your starting point.
Read more about shouldComponentUpdate on the official react docs page.
Use this as an opportunity to return false when you're certain that the transition to the new props and state will not require a component update.
I have the same issue and i find the (dumb) solution.
You just have a <button> (button by default is type=submit) or something similar (form, submit.... etc) thats is reloading the page like a html <form method=GET ...>.
Check it in your code, and remove it.
PD:
_k=somethingRandom > this is just the value inputs (or the button) that you are sending in the form.
I will give this a shot...
If you land here and looking to change your URL (for sharing purposes for example) then RR docs already has the solution described. Just make sure you do not use the history within the component (i.e. this.props.history.push())as you will be (as expected) routed to the target. You are however allowed to access your browser history without any interference with the component's history.
Following tested only on Chrome
// history.js
import { createBrowserHistory } from 'history'
export default createBrowserHistory()
and then from your XYZ component
// XYZ.js
import React from 'react';
import history from './history'
class XYZ extends React.Component {
_handleClick() {
// this should not cause rerender and still have URL change
history.push("/someloc");
}
render() {
return(
<button onClick={this._handleClick.bind(this)}>test </button>
)
}
}
Hope it helps someone else.
In App.js:
shouldComponentUpdate(nextProps, nextState) {
return nextProps.location.search === this.props.location.search
}
I think the easier workaround maybe replacing the Route with our own route
import { Route, withRouter } from "react-router-dom";
function MyRoute({ key, path, exact, component: Component, history }) {
let lastLocation = null;
return (
<Route
key={key}
path={path}
exact={exact}
render={(props) => {
history.listen((location) => {
lastLocation = location;
});
// monkey patching to prevent pushing same url into history stack
const prevHistoryPush = history.push;
history.push = (pathname, state = {}) => {
if (
lastLocation === null ||
pathname !==
lastLocation.pathname + lastLocation.search + lastLocation.hash ||
JSON.stringify(state) !== JSON.stringify(lastLocation.state)
) {
prevHistoryPush(pathname, state);
}
};
return <Component {...props} />;
}}
/>
);
}
export default withRouter(MyRoute);
We use this as a wrapper for actual Route of react-router-dom and it works perfectly for me.
for more please refer here
tsx sample
import {createBrowserHistory} from 'history';
export const history = createBrowserHistory();
ReactDOM.render(
<Router history={history}>
<App/>
</Router>,
document.getElementById("root")
);
Related
Is there a way to force a React-Router <Link> to load a page from path, even when the current location is already that page? I can't seem to find any mention of this in the react-router documentations.
We have a page on a route for "apply" that loads up a landing page with a hero image, some explanatory text, etc., and an "apply for this program" button that swaps in content that acts as an application form. This all happens on the same "apply" route, because users should not be able to directly navigate to this form without first hitting the landing page.
However, when they have this form open, and they click on the apply link in the nav menu again, the entire page should reload as it would on first mount, getting them "back" (but really, forward) to the landing page again.
Instead, clicking the <Link> does nothing, because react-router sees we're already on the "apply" page, and so does not unmount the current page to then mount a different one.
Is there a way to force it to unmount the current page before then mounting the requested page, even if it's for the page users are supposedly already on? (via a <Link> property for instance?)
Note: this question was posted when React-Router meant v5, and while the problem in this post is independent of a specific React-Router versions, but the solutions are not. As such, the accepted answer is the solution for React-Router v6, so if you're still using v5, first and foremost upgrade your version of React-Router, but if you absolutely can't, the accepted answer won't work for you and you'll want this answer instead.
In the Route component, specify a random key.
<Route path={YOURPATH} render={(props) => <YourComp {...props} keyProp={someValue} key={randomGen()}/>} />
when react see a different key, they will trigger rerender.
A fix I used to solve my little need around this was to change the location that React-Router looks at. If it sees a location that we're already on (as in your example) it won't do anything, but by using a location object and changing that, rather than using a plain string path, React-Router will "navigate" to the new location, even if the path looks the same.
You can do this by setting a key that's different from the current key (similar to how React's render relies on key) with a state property that allows you to write clear code around what you wanted to do:
render() {
const linkTarget = {
pathname: "/page",
key: uuid(), // we could use Math.random, but that's not guaranteed unique.
state: {
applied: true
}
};
return (
...
<Link to={linkTarget}>Page</Link>
...
);
}
Note that (confusingly) you tell the Link which values you need pass as a state object, but the link will pass those values on into the component as props. So don't make the mistake of trying to access this.state in the target component!
We can then check for this in the target component's componentDidUpdate like so:
componentDidUpdate(prevProps, prevState, snapshot) {
// Check to see if the "applied" flag got changed (NOT just "set")
if (this.props.location.state.applied && !prevProps.location.state.applied) {
// Do stuff here
}
}
Simple as:
<Route path="/my/path" render={(props) => <MyComp {...props} key={Date.now()}/>} />
Works fine for me. When targeting to the same path:
this.props.history.push("/my/path");
The page gets reloaded, even if I'm already at /my/path.
Based on official documentation for 'react-router' v6 for Link component
A is an element that lets the user navigate to another page by clicking or tapping on it. In react-router-dom, a renders an accessible element with a real href that points to the resource it's linking to. This means that things like right-clicking a work as you'd expect. You can use to skip client side routing and let the browser handle the transition normally (as if it were an ).
So you can pass reloadDocument to your <Link/> component and it will always refresh the page.
Example
<Link reloadDocument to={linkTo}> myapp.com </Link>
At least works for me!
Not a good solution because it forces a full page refresh and throws an error, but you can call forceUpdate() using an onClick handler like:
<Link onClick={this.forceUpdate} to={'/the-page'}>
Click Me
</Link>
All I can say is it works. I'm stuck in a similar issue myself and hope someone else has a better answer!
React router Link not causing component to update within nested routes
This might be a common problem and I was looking for a decent solution to have in my toolbet for next time. React-Router provides some mechanisms to know when an user tries to visit any page even the one they are already.
Reading the location.key hash, it's the perfect approach as it changes every-time the user try to navigate between any page.
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
this.setState({
isFormSubmitted: false,
})
}
}
After setting a new state, the render method is called. In the example, I set the state to default values.
Reference: A location object is never mutated so you can use it in the lifecycle hooks to determine when navigation happens
I solved this by pushing a new route into history, then replacing that route with the current route (or the route you want to refresh). This will trigger react-router to "reload" the route without refreshing the entire page.
<Link onClick={this.reloadRoute()} to={'/route-to-refresh'}>
Click Me
</Link>
let reloadRoute = () => {
router.push({ pathname: '/empty' });
router.replace({ pathname: '/route-to-refresh' });
}
React router works by using your browser history to navigate without reloading the entire page. If you force a route into the history react router will detect this and reload the route. It is important to replace the empty route so that your back button does not take you to the empty route after you push it in.
According to react-router it looks like the react router library does not support this functionality and probably never will, so you have to force the refresh in a hacky way.
I got this working in a slightly different way that #peiti-li's answer, in react-router-dom v5.1.2, because in my case, my page got stuck in an infinite render loop after attempting their solution.
Following is what I did.
<Route
path="/mypath"
render={(props) => <MyComponent key={props.location.key} />}
/>
Every time a route change happens, the location.key prop changes even if the user is on the same route already. According to react-router-dom docs:
Instead of having a new React element created for you using the
component prop, you can pass in a function to be called when the
location matches. The render prop function has access to all the same
route props (match, location and history) as the component render
prop.
This means that we can use the props.location.key to obtain the changing key when a route change happens. Passing this to the component will make the component re-render every time the key changes.
I found a simple solution.
<BrowserRouter forceRefresh />
This forces a refresh when any links are clicked on. Unfortunately, it is global, so you can't specify which links/pages to refresh only.
From the documentation:
If true the router will use full page refreshes on page navigation. You may want to use this to imitate the way a traditional server-rendered app would work with full page refreshes between page navigation.
Here's a hacky solution that doesn't require updating any downstream components or updating a lot of routes. I really dislike it as I feel like there should be something in react-router that handles this for me.
Basically, if the link is for the current page then on click...
Wait until after the current execution.
Replace the history with /refresh?url=<your url to refresh>.
Have your switch listen for a /refresh route, then have it redirect back to the url specified in the url query parameter.
Code
First in my link component:
function MenuLink({ to, children }) {
const location = useLocation();
const history = useHistory();
const isCurrentPage = () => location.pathname === to;
const handler = isCurrentPage() ? () => {
setTimeout(() => {
if (isCurrentPage()) {
history.replace("/refresh?url=" + encodeURIComponent(to))
}
}, 0);
} : undefined;
return <Link to={to} onClick={handler}>{children}</Link>;
}
Then in my switch:
<Switch>
<Route path="/refresh" render={() => <Redirect to={parseQueryString().url ?? "/"} />} />
{/* ...rest of routes go here... */}
<Switch>
...where parseQueryString() is a function I wrote for getting the query parameters.
There is a much easier way now to achieve this, with the reloadDocument Link prop:
<Link to={linkTarget} reloadDocument={true}>Page</Link>
you can use BrowserRouter forceRefresh={true}
I use react-router-dom 5
Example :
<BrowserRouter forceRefresh={true}>
<Link
to={{pathname: '/otherPage', state: {data: data}}}>
</Link>
</BrowserRouter>
Solved using the Rachita Bansal answer but with the componentDidUpdate instead componentWillReceiveProps
componentDidUpdate(prevProps) {
if (prevProps.location.pathname !== this.props.location.pathname) { window.location.reload();
}
}
You can use the lifecycle method - componentWillReceiveProps
When you click on the link, the key of the location props is updated. So, you can do a workaround, something like below,
/**
* #param {object} nextProps new properties
*/
componentWillReceiveProps = (nextProps)=> {
if (nextProps.location.pathname !== this.props.location.pathname) {
window.location.reload();
}
};
To be honest, none of these are really "thinking React". For those that land on this question, a better alternative that accomplishes the same task is to use component state.
Set the state on the routed component to a boolean or something that you can track:
this.state = {
isLandingPage: true // or some other tracking value
};
When you want to go to the next route, just update the state and have your render method load in the desired component.
Try just using an anchor tag a href link. Use target="_self" in the tag to force the page to rerender fully.
So I'm working on a react app using, among other things, react-router. I'm having a problem where I'm changing the route, the change is reflected in the URL, but the mounted component doesn't change. Here is the component, it's one of my main container components:
class AppContent extends Component {
state = {
isStarted: false
};
componentDidMount = async () => {
try {
await this.props.checkIsScanning();
this.setState({ isStarted: true });
}
catch (ex) {
this.props.showErrorAlert(ex.message);
}
};
componentWillUpdate(nextProps) {
if (nextProps.history.location.pathname !== '/' && !nextProps.isScanning) {
this.props.history.push('/');
}
}
render() {
return (
<div>
<VideoNavbar />
{
this.state.isStarted &&
<Container>
<Row>
<Col xs={{ size: 8, offset: 2 }}>
<Alert />
</Col>
</Row>
<Switch>
<Route
path="/scanning"
exact
render={ (props) => (
<Scanning
{ ...props }
isScanning={ this.props.isScanning }
checkIsScanning={ this.props.checkIsScanning }
/>
) }
/>
<Route path="/"
exact
render={ (props) => (
<VideoListLayout
{ ...props }
isScanning={ this.props.isScanning }
/>
) }
/>
</Switch>
</Container>
}
</div>
);
}
}
const mapStateToProps = (state) => ({
isScanning: state.scanning.isScanning
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
checkIsScanning,
showErrorAlert
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(AppContent));
So, let's focus on the important stuff. There is a redux property, isScanning, that is essential to the behavior here. By default, when I open the app, the route is "/", and the VideoListLayout component displays properly. From there, I click a button to start a scan, which changes the route to "/scanning", and displays the Scanning component. The Scanning component, among other things, calls the server on an interval to check if the scan is done. When it is complete, it sets "isScanning" to false. When AppContent re-renders, if "isScanning" is false, it pushes "/" onto the history to send the user back to the main page.
Almost everything here works. The Scanning component shows up when I start the scan, and it polls the server just fine. When the scan is complete, redux is properly updated so "isScanning" now is false. The componentWillUpdate() function in AppContent works properly, and it successfully pushes "/" onto the history. The URL changes from "/scanning" to "/", so the route is being changed.
However, the Scanning component remains mounted, and the VideoListLayout component is not. I can't for the life of me figure out why this is happening. I would've thought that once the route was changed, the components would change as well.
I'm sure I'm doing something wrong here, but I can't figure out what that is. Help would be appreciated.
I'm pretty sure you're running into this issue described in the react-router docs where react-redux's shouldComponentUpdate keeps your component from re-rendering on route change: https://reacttraining.com/react-router/core/guides/redux-integration/blocked-updates. This can definitely be a pain and pretty confusing!
Generally, React Router and Redux work just fine
together. Occasionally though, an app can have a component that
doesn’t update when the location changes (child routes or active nav
links don’t update).This happens if: The component is connected to
redux via connect()(Comp). The component is not a “route component”,
meaning it is not rendered like so: The problem is that Redux implements
shouldComponentUpdate and there’s no indication that anything has
changed if it isn’t receiving props from the router. This is
straightforward to fix. Find where you connect your component and wrap
it in withRouter.
So in your case, you just need to swap the order of connect and withRouter:
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppContent));
I have a route which takes an id and renders the same component for every id, for example :
<Route path='/:code' component={Card}/>
Now the in the Link tag I pass in an id to the component.Now the Card component fetches additional detail based on the id passed. But the problem is it renders only for one id and is not updating if I click back and goto the next id. I searched and found out that componentsWillReceiveProps can be used but during recent versions of React it has been deprecated. So how to do this?
Putting current location as key on component solves problem.
<Route path='/:code' component={(props) => <Card {...props} key={window.location.pathname}/>}/>
I just ran into a similar problem. I think you are conflating updating/rerendering and remounting. This diagram on the react lifecycle methods helped me when I was dealing with it.
If your problem is like mine you have a component like
class Card extend Component {
componentDidMount() {
// call fetch function which probably updates your redux store
}
render () {
return // JSX or child component with {...this.props} used,
// some of which are taken from the store through mapStateToProps
}
}
The first time you hit a url that mounts this component everything works right and then, when you visit another route that uses the same component, nothing changes. That's because the component isn't being remounted, it's just being updated because some props changed, at least this.props.match.params is changing.
But componentDidMount() is not called when the component updates (see link above). So you will not fetch the new data and update your redux store. You should add a componentDidUpdate() function. That way you can call your fetching functions again when the props change, not just when the component is originally mounted.
componentDidUpdate(prevProps) {
if (this.match.params.id !== prevProps.match.params.id) {
// call the fetch function again
}
}
Check the react documentation out for more details.
I actually figured out another way to do this.
We'll start with your example code: <Route path='/:code' component={Card}/>
What you want to do is have <Card> be a wrapper component, functional preferrably (it won't actually need any state I don't think) and render the component that you want to have rendered by passing down your props with {...props}, so that it gets the Router properties, but importantly give it a key prop that will force it to re-render from scratch
So for example, I have something that looks like this:
<Route exact={false} path="/:customerid/:courierid/:serviceid" component={Prices} />
And I wanted my component to rerender when the URL changes, but ONLY when customerid or serviceid change. So I made Prices into a functional component like this:
function Prices (props) {
const matchParams = props.match.params;
const k = `${matchParams.customerid}-${matchParams.serviceid}`;
console.log('render key (functional):');
console.log(k);
return (
<RealPrices {...props} key={k} />
)
}
Notice that my key only takes customerid and serviceid into account - it will rerender when those two change, but it won't re-render when courierid changes (just add that into the key if you want it to). And my RealPrices component gets the benefit of still having all the route props passed down, like history, location, match etc.
If you are looking for a solution using hooks.
If you are fetching data from some API then you can wrap that call inside a useEffect block and pass history.location.pathname as a parameter to useEffect.
Code:
import { useHistory } from "react-router";
const App = () => {
const history = useHistory();
useEffect(() => {
//your api call here
}, [history.location.pathname]);
};
useHistory hook from react-router will give the path name so the useEffect will be called everytime it (url) is changed
as described by #theshubhagrwl but
you can use location.href instead of location.pathname to work in all condition
import { useHistory } from "react-router";
const App = () => {
const history = useHistory();
useEffect(() => {
// do you task here
}, [history.location.href]);
};
You can use use UseLocation() from "react-router-dom"
and then use that object in useEffect dependency array.
import {useLocation} from "react-router-dom";
export default function Card() {
const location = useLocation();
useEffect(()=>{}, [location]);
return(
// your code here
);
}
In React Router v4 Adding a Switch tag after Router fixes the problem
I'm using react router v4, had some issue reloading the page (not window.location.reload). I better give a real use case to explain the issue, we use a social network app as the example:
user A commented a post by user B, a notification appear in user B page.
user B clicked on the notification, we do this.props.history.push('/job/' + id'), it worked, hence user B went to job/123 page.
user A commented again, new notification appear in user B page, while user B still remain on the job/123 page, he clicked on the notification link and triggered this.props.history.push('/job' + id'). But he won't see the page rerender, he DID NOT see the latest comment because the page does nothing.
It seems to be a common scenario in many cases. It can be tackled using many different approaches. Check this stackoverflow question. There are some good answers and findings. Personally this approach made more sense to me.
location.key changes every single time whenever user tries to navigate between pages, even within the same route. To test this place below block of code in you /jod/:id component:
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
console.log("... prevProps.key", prevProps.location.key)
console.log("... this.props.key", this.props.location.key)
}
}
I had this exact same situation. Updated state in componentDidUpdate. After that worked as expected. Clicking on items within the same route updates state and displays correct info.
I assume (as not sure how you're passing/updating comments in /job/:id) if you set something like this in your /job/:id component should work:
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
this.setState({
comments: (((this.props || {}).location || {}).comments || {})
})
}
}
You are describing 2 different kinds of state changes.
In the first scenario, when user B is not at the /job/:id page and he clicks a link you get a URL change, which triggers a state change in the router, and propagates that change through to your component so you can see the comment.
In the second scenario, when user B is already at the /job/:id page and a new comment comes through, the URL doesn't need to change, so clicking on a link won't change the URL and won't trigger a state change in the router, so you won't see the new content.
I would probably try something like this (pseudo code because I don't know how you're getting new comments or subscribing via the websocket):
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
class Home extends React.Component {
render() {
return (
<div>
<h1>The home page</h1>
{/* This is the link that the user sees when someone makes a new comment */}
<Link to="/job/123">See the new comment!</Link>
</div>
);
}
}
class Job extends React.Component {
state = { comments: [] };
fetchComments() {
// Fetch the comments for this job from the server,
// using the id from the URL.
fetchTheComments(this.props.params.id, comments => {
this.setState({ comments });
});
}
componentDidMount() {
// Fetch the comments once when we first mount.
this.fetchComments();
// Setup a listener (websocket) to listen for more comments. When
// we get a notification, re-fetch the comments.
listenForNotifications(() => {
this.fetchComments();
});
}
render() {
return (
<div>
<h1>Job {this.props.params.id}</h1>
<ul>
{this.state.comments.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
</div>
);
}
}
ReactDOM.render(
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/job/:id" component={Job} />
</Switch>
</BrowserRouter>,
document.getElementById("app")
);
Now the page will get updated in both scenarios.
this solved the problem for me :
import { BrowserRouter as Router,Route,Switch,Redirect} from "react-router-dom";
<Router>
<Switch>
<Redirect from="/x_walls/:user_Id" to='/walls/:user_Id'/>
<Route path="/walls/:user_Id" exact render={(props) => <Wall {...props}/> }/>
</Switch>
</Router>
and when you want to call "walls" you just call "x_walls" instead
You should watch updating postId in componentWillUpdate life cycle and do something from here like this:
componentWillUpdate(np) {
const { match } = this.props;
const prevPostId = match.params.postId;
const nextPostId = np.match.params.postId;
if(nextPostId && prevPostId !== nextPostId){
// do something
}
}
I have some kind of a wizard where each step has it's own route. Now when a user uses a direct link to the second step route, I would like to redirect him to the first step (using browserHistory.replace).
The problem I face is that I don't really know which stage in the components life cycle it should be performed. I have tried constructor, render, componentWillMount, but all of those do not prevent the component from rendering even if I use router. So what happens is that redirect occurs, but the component from the previous route still gets renders and fails (no data obviously).
Is there any "proper" way of redirecting during the component initialization (I need to check the state)? Or is there any other better way of doing so?
I don't know the "official" recommended way (still interested if someone can find it), but what worked for me is this:
Redirect during componentWillMount
During render use the same condition as in componentWillMount to return null
Example:
class MyComponent extends Component {
componentWillMount() {
if (!this.props.data) {
browserHistory.replace(myRoute);
}
}
render() {
if (this.props.data) {
return (
<div>
...
</div>
);
}
return null;
}
}
In v2/3 you should use the onEnter function to redirect (using the replace function). v2/3 replicates most of React's life cycle functions because it doesn't really use React to render route components. That is to say, <Route>s are just used for configuration and react-router generates an array of components for a given route and renders each individually.
function redirectToStart(nextState, replace) {
if (!nextState.someCondition) {
replace('/initial-page')
}
}
<Route onEnter={redirectToStart} ... />