Detecting user leaving page with react-router-dom v6.0.2 - javascript

I am trying to show Dialog Box for unsaved changes before navigating to other route or page in ReactJS web app. I tried different solution from Stackoverflow but didn't succeeded. Most of solution related to older version. I need for version 6.0.2 (react-router-dom). Anyone who can help me out in this really appreciated.

How to resolve my problem?
Instead of downgrading, I created two custom hooks.
useCallbackPrompt Hook
useBlocker Hook
useCallbackPrompt Hook
This hook handles a popup to show & hide and update location on the base of specific conditions.
How it looks like.
useBlocker Hook
This hooks blocks users from navigating away if there are any changes
How I am using useCallbackPrompt in my component
I created state showDialog; If the user changes something on the current page/form it will update the state showDialog and If the user tries to navigate away the prompt will be shown, and ask if the user wants to stay or not.
Here is the Live Demo Link
Here is the GITHUB REPO LINK
Link for Post

You can use usePrompt or useBlocker to detect and show a prompt before leaving to another route if they have any unsaved changes (for example in a unsaved form element).
However, in the official documentation of react router v6 the following is mentioned:
from v5 (along with usePrompt and useBlocker from the v6
betas) are not included in the current released version of v6.
I checked on Github as well, even the current version 6.2.1 doesn't support usePrompt and useBlocker.
But you can still downgrade to v5 or 6.0.0-alpha.5
the following code is working with the react router v6.0.0-alpha.5
import React, { useState, useRef, useEffect } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useHistory,
usePrompt,
useBlocker,
Routes
} from "react-router-dom";
// Sometimes you want to prevent the user from
// navigating away from a page. The most common
// use case is when they have entered some data
// into a form but haven't submitted it yet, and
// you don't want them to lose it.
export default function PreventingTransitionsExample() {
return (
<Router>
<ul>
<li>
<Link to="/">Form</Link>
</li>
<li>
<Link to="/one">One</Link>
</li>
<li>
<Link to="/two">Two</Link>
</li>
</ul>
<Routes>
<Route path="/" exact children={<BlockingForm />} />
<Route path="/one" children={<h3>One</h3>} />
<Route path="/two" children={<h3>Two</h3>} />
</Routes>
</Router>
);
}
function BlockingForm() {
let [isBlocking, setIsBlocking] = useState(false);
usePrompt(
"Hello from usePrompt -- Are you sure you want to leave?",
isBlocking
);
// useBlocker(
// () => "Hello from useBlocker -- are you sure you want to leave?",
// isBlocking
// );
return (
<form
onSubmit={event => {
event.preventDefault();
event.target.reset();
setIsBlocking(false);
}}
>
<p>
Blocking? {isBlocking ? "Yes, click a link or the back button" : "Nope"}
</p>
<p>
<input
size="50"
placeholder="type something to block transitions"
onChange={event => {
setIsBlocking(event.target.value.length > 0);
}}
/>
</p>
<p>
<button>Submit to stop blocking</button>
</p>
</form>
);
}

Related

React router url changes correctly but component not being rendered from button component link

I have something similar to the following CodeSandBox example where I need to use react routing within two different components.
The problem that I am facing is that, if using this codesandbox example and I click down to either the Profile or Quotes component level and assuming I have the following Material-UI button within each of these bottom components:
<Button>
text="New Item"
variant="outlined"
className={classes.newButton}
component={NewItem}
to={'/new-item'}
</Button>
When clicking on this "New Item" button, the URL changes to the correct route, i.e. /new-item but the actual NewItem component is not being rendered in the page?
If I refresh the browser, then it loads correctly.
Can anyone pls assist with the above and what the best solution is, when at this level?
Arthur, make sure in your index.js or wherever you are declaring your switch its set up like so.
<Switch>
<Route exact path="/NewItem" component={NewItem}/>
</Switch>
Then you can simply add a nav link like so
<Link to="/NewItem">New Item</Link>
the button will have a component of Link which is imported from react-router-dom
<Button>
text="New Item"
variant="outlined"
className={classes.newButton}
component={Link}
to="/new-item"
</Button>
and in the app.js you will create a Route like so :
<Switch>
<Route exact path="/NewItem" component={NewItem}/>
</Switch>
Well you shouldn't use button for changing go to new Page or going new URL. rather than use
<link to=""/> for routing but if you want to use button for going to different page than use Redirect from react-router-dom
import { Redirect } from 'react-router-dom';
const Next = (e) => {
e.preventDefault();
return <Redirect to='/NewItem' />;
};
<Button onClick={Next}>
text="New Item" variant="outlined" className={classes.newButton}
component={NewItem}
</Button>;
Or you can use Link using Link
import {Link} from 'react-router-dom';
<Link to={'/NewItem'} className={classes.newButton}>
New Item
</Link>;

How do I display a button which direct me to a search page instead of displaying a hyperlink?

Hello dear colleagues,
I have a question based on a project I'm doing in a course I'm rolled.
All of my code is practicaly build, the only thing that I'm with difficulties to implement is a button which I have to display and by clicking this button it'll take me to a search page. I've implemented the link, which appears as a hyperlink in the main page, but it'd be in place of this hyperlink the button.
My main page is displaying the hyperlink like this (note the hyperlink in the bottom of the page at the right side):
Main Page With Hyperlink
But, according to the project requirements, the main page would display the button like this (with functionallity to take me to the search page, as the hyperlink does):
Main Page With Button Displayed
Below I show to you part of the codes I've done for the components:
Part of App.js code:
The beggining of the code:
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './Home';
import Search from './Search';
import * as BooksAPI from './BooksAPI';
import './App.css';
class BooksApp extends React.Component {
state = {
books: []
}
The part with the Route:
render() {
return (
<div className="app">
<Route exact path="/" render={() => (
<Home
books={this.state.books}
changeShelf={this.changeShelf}
/>
)} />
<Route path="/search" render={() => (
<Search
changeShelf={this.changeShelf}
books={this.state.books}
/>
)} />
Part of main page code (Home.js):
The beggining of the code:
import React, {Component} from 'react';
import { Link } from 'react-router-dom';
import Book from './Book';
class Home extends Component {
The part which displays the link instead of the button:
<div className="open-search">
<Link to="/search">
Add a book
</Link>
</div>
So, I'd need some clarity here, what am I doing wrong in this code?
Please, if you'll need other code parts, please tell me that I put here, but I think these parts are enough for you to help me.
Regards.
You need to wrap your button in Link. See below.
<div className="open-search">
<Link to="/search">
<YourButtonComponent/>
</Link>
</div>
By doing this <YourButtonComponent/> is acting the same way that the text Add a Book does. It's wrapped in Link, so clicking on the <YourButtonComponent/> is the same as clicking the Link.

How to get local state of another component using Redux

Using react + redux, I am making an app where you answer questions, 5 questions per step and about 20 steps. When starting a new step, I save the progress to localStorage.
My app structure is like this:
class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<div id="ocean-model">
<Topbar />
<main id="main">
<Route exact path="/" component={Landing} />
<Switch>
<PrivateRoute exact path="/survey" component={Survey} />
</Switch>
<Switch>
<AdminRoute exact path="/edit-questions" component={EditQuestions} />
<AdminRoute exact path="/add-question" component={AddQuestion} />
</Switch>
</main>
</div>
</Router>
</Provider>
);
}
}
In Survey component I use controlled inputs to save current answers to local state and on submitting all 5 answers I use redux action to save given answers to localStorage and redux store.
In Topbar component I have a button, which onClick I would like to take the current progress from redux store, but also get the currently answered questions(for example 2 out of 5), which are only available in the Survey component's local state.
Do I need to modify the App structure so that Topbar and Survey can share state or maybe in Survey I need to somehow listen to an onclick event of a button from Topbar?
Is there any reason you can't save given answer to localStorage and redux store when the input loses focus? Might need to allow for the action to fire if the Topbar component button is clicked while the focus is still on the last input with an answer.
The alternative would be to use React Context as your state container for each set of 5 questions and use that from both the form and the Topbar button with a Provider located in the component tree above both.

React Router Changes URL, but BrowserRouter doesn't change Viewed Component

The project I am working on is kinda of sensitive, but here's what I have that I can show you all.
When I click on one of the s in my NavBar, it changes the URL, but it does not change the view to the proper component. Does anyone have any idea why? If there's a better way I can phrase this or improve the quality of my questin, let me know.
My render that I return
<NavBar />
<BrowserRouter>
<div>
<Switch>
<Route path="/propertytypes" component={PropertyTypes} />
<Route path="/entitytypes" component={EntityTypes} />
<Route path="/associationtypes" component={AssociationTypes} />
<Route path={Routes.ROOT} component={Test} />
</Switch>
</div>
</BrowserRouter>
My NavBar
import React, { Component } from "react";
import { Link } from "react-router-dom";
class NavBar extends Component {
render() {
return (
<div>
<ul>
<li>
<Link to="/propertytypes">Props!</Link>
</li>
<li>
<Link to="/entitytypes">Ent!</Link>
</li>
<li>
<Link to="/associationtypes">Ass!</Link>
</li>
</ul>
</div>
);
}
}
export default NavBar;
After clicking Props link
Besides what #Ukasyah noticed about disparity between the Router you are using (you say you are using BrowserRouter) and the screen captures with the URL you are getting (that points out you are using HashRouter), you must be getting a problem with the code you are showing up because the <Link> component in order to work must be contained inside the BrowserRouter. In your browser console it must be happening this error:
Warning: Failed context type: The context router is marked as
required in Link, but its value is undefined.
So to avoid the problem and links start to work, your render should be something like this:
<BrowserRouter>
<div>
<NavBar />
<Switch>
<Route path="/propertytypes" component={PropertyTypes} />
<Route path="/entitytypes" component={EntityTypes} />
<Route path="/associationtypes" component={AssociationTypes} />
<Route path={Routes.ROOT} component={Test} />
</Switch>
</div>
</BrowserRouter>
Note that I put the NavBar component inside the div because BrowserRouter only allows to have a single child.
I have been looking at different SO questions to solve this issue and none of them helped. My problem was using BrowserRouter in multiple different components. Same as here.
For me, the issue was that my entire app was wrapped with a <Router>, because that was how the boilerplate code was set up. Then I was copying snips from BrowserRouter docs, and did not remove the <BrowserRouter> wrapper around the code. Nested routers won't work properly.
The mistake I made was, I started with the setup from the documentation, which involves using:
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
Then I started splitting everything into separate layout files like this. So I initially copied the whole import between multiple files. At some point I was cleaning up the code and realized I only needed to import the Switch but I foolishly forgot to remove the BrowserRouter as, and I ended up with:
import { BrowserRouter as Switch } from "react-router-dom";
Which is obviously wrong since it mapped BrowserRouter to Switch, you can't have nested BrowserRouter 's and that's what introduced the issue in my case.

Navigate with javascript using the MemoryRouter in react-router

I'm building a single page web application using React. For this application, I'd like to prevent the URL from changing and also avoid using href links to prevent the "link preview" from showing up in the bottom left hand corner of browsers.
To address the first half of my problem I found that using MemoryRouter from react-router prevented URLs from changing when navigating within the application.
Here's a PoC for the router:
import App from './stuff/main/App';
import {Route, MemoryRouter} from "react-router";
import default_page from "./stuff/pages/default_page"
import another_page from "./stuff/pages/another_page"
const app = document.getElementById('root');
ReactDOM.render(
<MemoryRouter>
<App>
<Route exact path="/" component={default_page} />
<Route path="/anotherpage" component={another_page} />
</App>
</MemoryRouter>,
app
);
Then in App I have code like this (which works):
...
<Link to="/">default_page</Link>
<Link to="/anotherpage">another_page</Link>
{this.props.children}
...
I cannot figure out how to change pages in javascript using the MemoryRouter. The only method I've been able to find that works is using the Link tag, which causes the url to show up on the bottom left hand of the screen. If I could use another element and onclick, I'd be golden.
If you are in child Component of <Route>, you should have history prop available:
<button onClick={()=>this.props.history.push('/anotherpage')} >another_page</button>
If you are not, you can pass props.history from parent element.
Or if you are deep in structure you should use withRouter HOC
Or you can create a new without path prop, so it will match always and it provide you the right history object.
<Route render={({history})=>(
<button onClick={()=>history.push('/anotherpage')} >another_page</button>
// more buttons
)} />

Categories

Resources