React-Router not unmounting on location parameter change - javascript

This problem is driving me insane, I got a simple little app created by create-react-app and I am using React-Router.
JSX:
<Router>
<Route path="/pages/:pageId" component={Page} />
</Router>
In the Page component I have an fetch on componentDidMount, its working as expected first time the route is loaded.
For example I visit: /pages/13 and then it fetches that pages data, but when I am on: /pages/13 and then click on a Link component (imported from React-Router) redirecting me to: /pages/15 - the pageId prop is then updated but it never re-renders.
The Page component never run componentWillUnmount before, or componentDidMount after.
Expected behavior:
Visit: /pages/13 → render() → componentDidMount() → render()
Click on Link component /pages/15 → componentWillUnmount() → render()
→ componentDidMount() → render()
I am using the following versions:
react: 16.3.2
react-dom: 16.3.2
react-router-dom: 4.2.2
What am I doing wrong? I am still pretty new to React so be nice :)

Your problem is that componentDidMount only executes when the component is completely mounted, and this only occurs one time. In this case, changing the route param doesn't change the component, I mean, you still being in the same component when you change the pageId, thus the componentDidMount is never executed again.
What could you do then?, well, I suggest you to look into componentWillReceiveProps(newProps) method, this is executed whenever the component's props are about to be modified, which in this case, is what you are looking for, because, according to this:
You’ll have access match objects in various places:
Route component as this.props.match;
Route render as ({ match }) => ();
Route children as ({ match }) => ();
withRouter as this.props.match;
matchPath as the return value;
the match object (which contains the route params) is given through component's props.
So, short answer. Use componentDidMount for first time fetching, and then use componentWillRecieveProps(newProps) for all the rest fetching calls.
check this for more info.
Note: I didn't know that componentWillRecieveProps(newProps) was about to be somewhat deprecated, anyways, it suggest you to use getDerivedStateFromProps(nextProps, prevState) which is basically the same

When parameter is changing, your Route doesn't remount your component. This is an expected behaviour. You should catch the parameter changing via componentWillReceiveProps, otherwise if you wish to force remounting, you should use <Route path="/your/path/with/:id" render={props => <Component {...props} key={props.params.id} />

Related

how to re render component on Link click [duplicate]

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.

history.push and Link change the URL and not re render the component if I have many routes for the same component

// the routes here
<Switch>
<Route exact path='/:page' component={Dashboard} />
<Route exact path='/:subCateg/:categ' component={Dashboard} />
</Switch>
I want to go from the first route to the second one, if I used (history.push) or (Link) just the URL change and not reloading.
Also, the same problem if I want to go from the second route to itself but with new params.
//here I'm in Dashbord and the History also pushing to Dashboard
onClick={
()=>{
history.push({pathname:`/${categ}/${subCateg}`, state: {id: subCateg.id}})
// window.location.reload(false);
}
}
as you can see I userd "window.location.reload(false);" and it solved the loading problem, but what if I want to Goback by browser Goback button, the same problem : change URL & not relaoding.
also I think using "window.location.reload(false);" is not a good practice.
``
package.json:
"react-router-dom": "^5.1.2",
As you are using react-router-dom you should not be using history.push(). Try to use Redirect from react-router-dom instead.
Just use a state named redirect and set it to the new route you want to go with your function:
onClick={() => this.setState({ redirect: 'newpath' }};
Then just put a return inside your render just like this:
if (this.state.redirect) {
return <Redirect to={this.state.redirect} />;
}
return (
...rest of your render
);
Caveat: I'm dangerously new to React as well as much around it, but have been pulling my hair out on this for the last day myself. So there are a number of reasons this may be very bad advice. But this was my situation and how I got it to work.
function newSongRedirect() {
history.push("/SongDetail/0");
history.go(0);
}
and hook it up to my button:
<button onClick={() => newSongRedirect() } >New Song</button>
It appears that history.push adds the value to the history, but doesn't cause it to be acted on. As now the first item, history.go(0) then causes it to act on it.
I've seen some posts referencing that history.push can't be used if you're nested in a <BrowserRouter>, but in my case, attempting to replace that in my index.js just led to other issues.
Also, I found this post from what looks to be part of the team working on react router. It seems the useHistory hook will be replaced when they get a chance.
Heads up: The useHistory hook is a quick stopgap for a future hook that we are working on: useNavigate. useNavigate will provide an API that is more closely aligned with and will fix a few long-standing problems with using the history API directly in the router (it'll probably look a lot like #reach/router's navigate API). We are providing useHistory for now to make the migration of existing code that uses the history API as painless as possible.

react-router-dom <Link/> is clearing the {history,match,location} props when clicked in ssr application

I could not implement the Link component in server-side rendering.
<Link to={`/edit/${id}`}>
<h3>{description}</h3>
</Link>
In /edit page, I have this line of code to test the props that passed:
<h1>{props.match.params.id}</h1>
this throws an error because match prop is not passed.
If I used <a></a> instead of <Link/> wrapped /edit page with withRouter I get those props however this time I am disconnected from the store.
Since <Link/> navigates inside react-router looks like props that passed to components are cleared up when I click on <Link/>. I could not figure out how to solve the issue.
I added historyApiFallback:true to webpack.config devServer object but it did not solve the issue.
here is the repo
Your exact mistake is using href prop for the react-router Link component. you should use to, like below:
<Link to={`/edit/${id}`}>
<h3>{description}</h3>
</Link>
After fixing this issue, directly you will fall into another issue. you will get it inside EditExpensePage page:
const mapStateToProps = (state, props) => {
return {
expense: state.expenses.find(
expense => expense.id === props.match.params.id // here, you will get error the params of undefined
)
};
};
You will get params of undefined error because you wrap the withRouter by using react-redux connect, in fact, you should wrap react-redux connect by a withRouter:
export default withRouter(connect(mapStateToProps)(EditExpensePage));
After moving HOC wrapping the match key of props won't be undefined.

Why won't it pass a function to a child React component rendered by React Router as a prop? [duplicate]

I really don't get the difference between render and component prop in Route in react router, in docs it says that render doesn't create new element but component does, I tried to go back with history but I found componentWillMount is called when I use render in Route, what do they mean by "if you provide an inline function to the component attribute, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component."
The source code tells the difference:
if (component)
return match ? React.createElement(component, props) : null
if (render)
return match ? render(props) : null
When you use component prop, the component is instantiated per every call of Route#render. It means that, for your component that you pass to component prop of Route, constructor, componentWillMount, and componentDidMount will be executed every time the route is rendered.
For example, if you have
<Route path="/:locale/store" component={Store} />
and the user navigates to /en/store, then goes elsewhere, and then navigates back to /en/store, the Store component will be mounted, then unmounted, and then mounted again. It is similar to doing
<Route path="/:locale/store">
<Store />
</Route>
Compared to that, if you use render prop, the component is evaluated on every Route#render. Remember that every component is a function? This function will be executed as is, without any lifecycle methods. So when you have it like
<Route path="/:locale/store" render={Store} />
you can think of it as
<Route path="/:locale/store">
{Store()}
</Route>
It saves you runtime because no lifecycle methods are run, but it also has a downside in case Store component has some post-mount lifecycle methods like shouldComponentUpdate that may increase performance as well.
There was a good post on Medium about this performance hack, please take a look at it. It's written very well and is applicable to React 16, too.
So I'm confused on this section of docs as well, but I finally figure it out.
The key to understand this is the statement "provide an inline function to the component prop"
We all know that Route component will re-render when the location changed, and react will compare the old and new virtual DOM tree, get some diff result and apply to the real DOM.
And react will try it's best to reuse the DOM node, unless the type or key prop of the new ReactElement is changed.
So
// 1.
const componentA = React.createElement(App, props)
const componentB = React.createElement(App, props)
console.log(componentA.type === componentB.type) // true
// 2.
const componentA = React.createElement(() => <App />, props)
const componentB = React.createElement(() => <App />, props)
console.log(componentA.type === componentB.type) // false
All ReactElements created by way 1 have the same type(App component), but they don't have the same type if they are all created by way 2.
Why?
Because there is always a new anonymous function created in the way 2 when the parent component's(The component that contains Route component) render method got invoked, so the type of new&old ReactElement is two different instances of the anonymous function
() => <App />
So in React's point of view, there are different types element and should be treat with unmount old > mount new operation, that means every state or changes you made on the old component got loss everytime the parent component re-render.
But why the render prop avoid the unmount and mount behavior? It's an anonymous function too!?
Here I would like to refer the code that #Rishat Muhametshin posted, the core part of Route component's render method:
if (component)
// We already know the differences:
// React.createElement(component)
// React.createElement(() => <component/>)
return match ? React.createElement(component, props) : null
if (render)
return match ? render(props) : null
render prop is a function that return a ReactElement when invoked, what's the type of that returned element?
<Route render={() => <AppComponent />}></Route>
It's AppComponent, not the anonymous function wrapper! Because after jsx compiled:
render = () => React.createElement(AppComponent)
render() = React.createElement(AppComponent)
React.createElement(render) =
React.createElement(() => React.createElement(AppComponent))
React.createElement(render()) =
React.createElement(React.createElement(AppComponent))
So when you use render instead of component prop, the type of element that render prop function return will not change on each render, even there always an new anonymous function instance created on each parentElement.render()
On my point of view, you can achieve the same behavior that render prop does with component prop by giving a name to the anonymous function:
// Put this line outside render method.
const CreateAppComponent = () => <AppComponent />
// Inside render method
render(){
return <Route component={CreateAppComponent}/>
}
So the conclusion is, there is not performance different between component and render prop if you are use component={AppComponent} directly, if you want to assign some props to AppComponent, use
render={() => <AppComponent {...props}/> } instead of component={() => <AppComponent {...props}/> }
Most of concepts have been explained by other answers, Let me sort it out by following:
First of all, we have source code:
if (component)
return match ? React.createElement(component, props) : null
if (render)
return match ? render(props) : null
case #1: component without function
<Route path="/create" component={CreatePage} />
React.createElement(CreatePage, props) be called because of React.createElement(component, props) from source code. The instantiation would cause to be remounting.
case #2: render without function
<Route path="/create" render={CreatePage} />
React.createElement(CreatePage, props) was called before passing into render prop, and then called by render(props) from source code. No instantiation, no remounting.
case #3: component with function
<Route path="/create" component={ () => <CreatePage /> } />
React.createElement(CreatePage, props) be called twice. First for jsx parsing(anonymous function), First for returning an instance of CreatePage from anonymous function, second from source code. So why don't do this in component prop.
Errors point out by oligofren:
Parsing the JSX does not call it. It just ends up creating the function expression. The reason you don't want to do #3 is that you create a new anonymous type each time, causing re-mounting of the dom.
case #4: render with function
<Route path="/create" render={ () => <CreatePage /> } />
There is an instantiation(jsx parsing) each time when routing to path=/create. Does it feel like case #1?
Conclusion
According to the four cases, If we want to pass prop to Component, we need to use the case #4 to prevent remounting.
<Route path="/abc" render={()=><TestWidget num="2" someProp={100}/>}/>
This is a bit far from the topic, so I leave the official discussion for further reading.
Even if we don't pass any props to ComponentToRender, I found some benefits from using render instead of component.
By default <Route \> pass additional props({ history, location, match }) to ComponentToRender when using component. We can access this props via render callback too, but also we can omit it.
Why do we need it? Every render of <Route />'s parent or any navigation(even if change route to same as before) create new match object.
So when we pass it to our ComponentToRender, we will get new props every time, what may cause some performance issues, especially with PureComponent.
Even if we give a name to a function it would create a new reference when this component would be called from any parent component. That new reference would be passed to the component. But we can avoid the creation of new references by using use callback hook.
// Put this line outside render method.
const CreateAppComponent = () => <AppComponent />
// Inside render method
render(){
return <Route component={CreateAppComponent}/>
}

Prevent react-router history.push from reloading current route

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")
);

Categories