Stop loading a new route/component if current component has changes - javascript

I have a requirement where I need to check if the local state has changes before the user navigates to the next tab. sort of like handing a component abandonment. I have come up with 2 options as follows,
Achieve this via componentWillUnmount. If its a good practice is there a way to conditionally stop the component being unmounted?
Via the window. As stated in the following solution : https://groups.google.com/forum/#!topic/reactjs/z63RGG1l_0U
Any idea on this matter is greatly appreciated :)

In the react-router-redux router is part of the state, so you can experiment on that.
To do so you should take a look at RouterContext, which provides setRouteLeaveHook function.
OR
As far as I remember there's also second option. Router object on context contains listenBeforeLeavingRoute
this.context.router.listenBeforeLeavingRoute
But its basically the same thing if you look at the source code of react-router. But its accessible from different layers.
EDIT:also Route has onLeave hook, may be useful!
Hope it helps somehow.
Regards,
Mariusz

How does the user navigate to the next tab? When you are using a <Link> you could define an unsavedChanges flag in your state. Set this to true (via dispatching an action and having a reducer responsible for that action) whenever you think that the user must not leave the current tab.
class Foo extends React.Component {
handleClick(e) {
const { unsavedChanges } = this.props
if(unsavedChanges) {
e.preventDefault()
}
}
render() {
return (
<Link to='/nextTab' onClick={this.handleClick}>Bar</Link>
)
}
}
Of course you need to pass unsavedChanges to your components props.

Related

HashRouter, base root "/" [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.

What happens to the code that is written after execution of history push in React?

I generally use the Redirect component for routing. As we need to return the Redirect, it is pretty understandable. However, I am confused with using history objects. Actually in my code base after pushing some route into the history stack. There are so many actions and functions and dispatchers are being called.
When would the new routes component be mounted? If someone could not understand please see the below sample code.
const some_handler_func = () => {
history.push('/component 2');
setState('dummy');
console.debug('checking')
//what happens to the above two lines.
}
The console.debug will run as intended, but state change to your component might not work, because the component you try to change state on might be already unmounted.

React Router v6.0.0-alpha.5 history prop removal - navigating outside of react context

According to the latest v6.0.0-alpha.5 release of react router, the history prop has been removed:
https://github.com/ReactTraining/react-router/releases/tag/v6.0.0-alpha.5
Removed the <Router history> prop and moved responsibility for setting
up/tearing down the listener (history.listen) into the wrapper
components (<BrowserRouter>, <HashRouter>, etc.). <Router> is now a
controlled component that just sets up context for the rest of the
app.
Navigating within the react context is simple with the useNavigate hook.
But, how does the removal of the history prop affect programmatically navigating outside of the react context?
For example, how would we keep our history in sync in order to navigate from inside redux, or an axios/http interceptor, etc., when we no longer can pass the history object?
Current V5 implementation:
https://reacttraining.com/react-router/web/api/Router
Or, from v6 onwards is the goal to rely on navigating from within react components only?
Thanks for the question, we know this is going to come up a lot. This is a common question we've gotten for years. Please be patient with us as we begin documenting all of these kinds of things, there's a lot to do!
Short answer: Typically people use thunks for async work that leads to wanting to navigate somewhere else (after a login, after a record is created, etc.). When your thunk is successful, change the state to something like "success" or "redirect" and then useEffect + navigate:
export function AuthForm() {
const auth = useAppSelector(selectAuth);
const dispatch = useAppDispatch();
const navigate = useNavigate();
useEffect(() => {
if (auth.status === "success") {
navigate("/dashboard", { replace: true });
}
}, [auth.status, navigate]);
return (
<div>
<button
disabled={auth.status === "loading"}
onClick={() => dispatch(login())}
>
{auth.status === "idle"
? "Sign in"
: auth.status === "loading"
? "Signing in..."
: null}
</button>
</div>
);
}
A bit more explanation:
For example, how would we keep our history in sync in order to navigate from inside redux
We've always considered this bad practice and reluctantly provided the history objects as first-class API to stop having philosophical conversations about app state and the URL 😅.
But today things are a bit different. The conversation isn't just philosophical anymore but has some concrete bugs when mixed with React's recent async rendering, streaming, and suspense features. To protect react router apps from synchronization bugs with the URL (that developers can't do anything about), v6 no longer exposes the history object.
Hopefully this explanation will help:
Changing the URL is a side-effect, not state. Thunks are used to perform side-effects that eventually figure out some state for the state container but aren't used for the side-effect in and of itself (at least that's my understanding).
For example, you may want to change the focus on the page after your redux state changes. You probably wouldn't try to synchronize and control the document's focus at all times through redux actions and state. Scroll position is the same. Ultimately the user is in control of these things: they can hit the tab key, click on something, or scroll around. Your app doesn't try to own or synchronize that state, it just changes it from time to time in response to actions and state that you do control.
The URL is the same. Users can type whatever they want into the address bar, click back, forward, or even click and hold the back button to go 3 entries back! It's the same kind of state as focus and scroll positions: owned by the user. The container can't ever truly own the URL state because it can't control the actions surrounding it. Mix in React's new and upcoming features and you're gonna lose that game.
In a nutshell: Change redux state > useEffect in the UI > navigate. Hope that helps!
I solved this for my login redirect by creating a navigate hook in my LoginForm UI component, then passing it to my login action creator, and calling it when the login endpoint returns successfully. In other words...
import React from 'react'
import {useNavigate} from 'react-router-dom'
function LoginForm (props) {
// create your navigate hook in your UI component
const navigate = useNavigate()
// other stuff...
handleLogin (loginFields) {
// then pass it into your action file, and call it
// when the query returns a successful result
dispatch(login(loginFields, navigate))
}
// other stuff, and return statement...
}

this.props.history.push is working just after refresh

I have a react-big-calendar, which has a button when I click on it, his modal will appear, and I have also a calendar icon button which redirects me to another URL /planning.
I have a dialog (AjouterDisponibilite) and I call it with the ref on my component Agenda.
And the calendar icon has an onClick event which I try:
this.props.history.push('/planning');
but when I run it and I click on the icon, the URL is directed correct, but I get an error as shown below:
TypeError: Cannot read property 'handleAjouter' of null and
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method
and after I refresh this page, it works.
My code is: https://codesandbox.io/s/xw01v53oz
How can I fix it?
The problem is improper use of Refs. Refs are designed to keep references to DOM-elements and component instances, not their functions.
Components with a ref prop will assign themself to the value of the prop (or call the function with itself as an argument) when they are mounted, and they will do the same with a null reference when they are unmounted.
With this knowledge, you have to change your code of the Agenda component as follows:
ModalAjout = (ref) => {
if (ref) {
this.ajout = ref.handleAjouter;
} else {
this.ajout = null;
}
}
ModalAjoutb = (ref) => {
if (ref) {
this.ajoutb = ref.handleAjouter;
} else {
this.ajoutb = null;
}
}
The best way to solve this you can add this code:
history.pushState(null, document.title, location.href);
What is doing? It's initializing the history with one value after it was loaded the first time, then you don't need to do the refresh.
In my case I did this while I was developing an Android App and had some modals that I wanted to close with the back button to provide a more native experience.
I fix it, by adding:
<AjouterDisponibilite ref={(evt) => this.ModalAjout({evt})} start={this.state.startDisponibilite} end={this.state.endDisponibilite} date={this.state.dateDisponibilite}/>
<AjouterDisponibilite ref={(evt) => this.ModalAjoutb({evt})} />
The method ModalAjout must have a parameter.

ReactJs: change state in response to state change

I've got a React component with an input, and an optional "advanced input":
[ basic ]
Hide Advanced...
[ advanced ]
The advanced on the bottom goes away if you click "Hide Advanced", which changes to "Show Advanced". That's straightforward and working fine, there's a showAdvanced key in the state that controls the text and whether the advanced input is rendered.
External JS code, however, might change the value of advanced, in which case I want to show the [advanced] input if it's currently hidden and the value is different than the default. The user should be able to click "Hide Advanced" to close it again, however.
So, someone external calls cmp.setState({advanced: "20"}), and I want to then show advanced; The most straightforward thing to do would just be to update showAdvanced in my state. However, there doesn't seem to be a way to update some state in response to other state changes in React. I can think of a number of workarounds with slightly different behavior, but I really want to have this specific behavior.
Should I move showAdvanced to props, would that make sense? Can you change props in response to state changes? Thanks.
Okay first up, you mention that a third party outside of your component might call cmp.setState()? This is a huge react no-no. A component should only ever call it's own setState function - nothing outside should access it.
Also another thing to remember is that if you're trying change state again in response to a state change - that means you're doing something wrong.
When you build things in this way it makes your problem much harder than it needs to be. The reason being that if you accept that nothing external can set the state of your component - then basically the only option you have is to allow external things to update your component's props - and then react to them inside your component. This simplifies the problem.
So for example you should look at having whatever external things that used to be calling cmp.setState() instead call React.renderComponent on your component again, giving a new prop or prop value, such as showAdvanced set to true. Your component can then react to this in componentWillReceiveProps and set it's state accordingly. Here's an example bit of code:
var MyComponent = React.createClass({
getInitialState: function() {
return {
showAdvanced: this.props.showAdvanced || false
}
},
componentWillReceiveProps: function(nextProps) {
if (typeof nextProps.showAdvanced === 'boolean') {
this.setState({
showAdvanced: nextProps.showAdvanced
})
}
},
toggleAdvancedClickHandler: function(e) {
this.setState({
showAdvanced: !this.state.showAdvanced
})
},
render: function() {
return (
<div>
<div>Basic stuff</div>
<div>
<button onClick={this.toggleAdvancedClickHandler}>
{(this.state.showAdvanced ? 'Hide' : 'Show') + ' Advanced'}
</button>
</div>
<div style={{display: this.state.showAdvanced ? 'block' : 'none'}}>
Advanced Stuff
</div>
</div>
);
}
});
So the first time you call React.renderComponent(MyComponent({}), elem) the component will mount and the advanced div will be hidden. If you click on the button inside the component, it will toggle and show. If you need to force the component to show the advanced div from outside the component simply call render again like so: React.renderComponent(MyComponent({showAdvanced: true}), elem) and it will show it, regardless of internal state. Likewise if you wanted to hide it from outside, simply call it with showAdvanced: false.
Added bonus to the above code example is that calling setState inside of componentWillReceiveProps does not cause another render cycle, as it catches and changes the state BEFORE render is called. Have a look at the docs here for more info: http://facebook.github.io/react/docs/component-specs.html#updating-componentwillreceiveprops
Don't forget that calling renderComponent again on an already mounted component doesn't mount it again, it just tells react to update the component's props and react will then make the changes, run the lifecycle and render functions of the component and do it's dom diffing magic.
Revised answer in comment below.
My initial wrong answer:
The lifecycle function componentWillUpdate will be ran when new state or props are received. You can find documentation on it here: http://facebook.github.io/react/docs/component-specs.html#updating-componentwillupdate
If, when the external setState is called, you then set showAdvanced to true in componentWillUpdate, you should get the desired result.
EDIT: Another option would be to have the external call to setState include showAdvanced: true in its new state.

Categories

Resources