How to use a custom component with react-router route transitions? - javascript

The article Confirming Navigation explains how to use a browser confirmation box in your transition hook. Fine. But I want to use my own Dialog box. If I were to use the methods from the history module I think this is possible. Is it possible to do this with the setRouteLeaveHook in react-router?

The core problem is that setRouteLeaveHook expects the hook function to return its result synchronously. This means you don't have the time to display a custom dialog component, wait for the user to click an option, and then return the result. So we need a way to specify an asynchronous hook. Here's a utility function I wrote:
// Asynchronous version of `setRouteLeaveHook`.
// Instead of synchronously returning a result, the hook is expected to
// return a promise.
function setAsyncRouteLeaveHook(router, route, hook) {
let withinHook = false
let finalResult = undefined
let finalResultSet = false
router.setRouteLeaveHook(route, nextLocation => {
withinHook = true
if (!finalResultSet) {
hook(nextLocation).then(result => {
finalResult = result
finalResultSet = true
if (!withinHook && nextLocation) {
// Re-schedule the navigation
router.push(nextLocation)
}
})
}
let result = finalResultSet ? finalResult : false
withinHook = false
finalResult = undefined
finalResultSet = false
return result
})
}
Here is an example of how to use it, using vex to show a dialog box:
componentWillMount() {
setAsyncRouteLeaveHook(this.context.router, this.props.route, this.routerWillLeave)
}
​
routerWillLeave(nextLocation) {
return new Promise((resolve, reject) => {
if (!this.state.textValue) {
// No unsaved changes -- leave
resolve(true)
} else {
// Unsaved changes -- ask for confirmation
vex.dialog.confirm({
message: 'There are unsaved changes. Leave anyway?' + nextLocation,
callback: result => resolve(result)
})
}
})
}

I made it work by setting a boolean on state whether you have confirmed to navigate away (using react-router 2.8.x). As it says in the link you posted:
https://github.com/ReactTraining/react-router/blob/master/docs/guides/ConfirmingNavigation.md
return false to prevent a transition w/o prompting the user
However, they forget to mention that the hook should be unregistered as well, see here and here.
We can use this to implement our own solution as follows:
class YourComponent extends Component {
constructor() {
super();
const {route} = this.props;
const {router} = this.context;
this.onCancel = this.onCancel.bind(this);
this.onConfirm = this.onConfirm.bind(this);
this.unregisterLeaveHook = router.setRouteLeaveHook(
route,
this.routerWillLeave.bind(this)
);
}
componentWillUnmount() {
this.unregisterLeaveHook();
}
routerWillLeave() {
const {hasConfirmed} = this.state;
if (!hasConfirmed) {
this.setState({showConfirmModal: true});
// Cancel route change
return false;
}
// User has confirmed. Navigate away
return true;
}
onCancel() {
this.setState({showConfirmModal: false});
}
onConfirm() {
this.setState({hasConfirmed: true, showConfirmModal: true}, function () {
this.context.router.goBack();
}.bind(this));
}
render() {
const {showConfirmModal} = this.state;
return (
<ConfirmModal
isOpen={showConfirmModal}
onCancel={this.onCancel}
onConfirm={this.onConfirm} />
);
}
}
YourComponent.contextTypes = {
router: routerShape
};

Posting my solution for intercept back button or even a route change. This works with React-router 2.8 or higher. Or even with withRouter
import React, {PropTypes as T} from 'react';
...
componentWillMount() {
this.context.router.setRouteLeaveHook(this.props.route, this.routerWillLeaveCallback.bind(this));
}
routerWillLeaveCallback(nextLocation) {
let showModal = this.state.unsavedChanges;
if (showModal) {
this.setState({
openUnsavedDialog: true,
unsavedResolveCallback: Promise.resolve
});
return false;
}
return true;
}
}
YourComponent.contextTypes = {
router: T.object.isRequired
};

Above is great except when user goes back in history. Something like the following should fix the problem:
if (!withinHook && nextLocation) {
if (nextLocation.action=='POP') {
router.goBack()
} else {
router.push(nextLocation)
}
}

Here's my solution to the same. I made a custom dialog component that you can use to wrap any component in your app. You can wrap your header and this way have it present on all pages. It assumes you're using Redux Form, but you can simply replace areThereUnsavedChanges with some other form change checking code. It also uses React Bootstrap modal, which again you can replace with your own custom dialog.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withRouter, browserHistory } from 'react-router'
import { translate } from 'react-i18next'
import { Button, Modal, Row, Col } from 'react-bootstrap'
// have to use this global var, because setState does things at unpredictable times and dialog gets presented twice
let navConfirmed = false
#withRouter
#connect(
state => ({ form: state.form })
)
export default class UnsavedFormModal extends Component {
constructor(props) {
super(props)
this.areThereUnsavedChanges = this.areThereUnsavedChanges.bind(this)
this.state = ({ unsavedFormDialog: false })
}
areThereUnsavedChanges() {
return this.props.form && Object.values(this.props.form).length > 0 &&
Object.values(this.props.form)
.findIndex(frm => (Object.values(frm)
.findIndex(field => field && field.initial && field.initial !== field.value) !== -1)) !== -1
}
render() {
const moveForward = () => {
this.setState({ unsavedFormDialog: false })
navConfirmed = true
browserHistory.push(this.state.nextLocation.pathname)
}
const onHide = () => this.setState({ unsavedFormDialog: false })
if (this.areThereUnsavedChanges() && this.props.router && this.props.routes && this.props.routes.length > 0) {
this.props.router.setRouteLeaveHook(this.props.routes[this.props.routes.length - 1], (nextLocation) => {
if (navConfirmed || !this.areThereUnsavedChanges()) {
navConfirmed = false
return true
} else {
this.setState({ unsavedFormDialog: true, nextLocation: nextLocation })
return false
}
})
}
return (
<div>
{this.props.children}
<Modal show={this.state.unsavedFormDialog} onHide={onHide} bsSize="sm" aria-labelledby="contained-modal-title-md">
<Modal.Header>
<Modal.Title id="contained-modal-title-md">WARNING: unsaved changes</Modal.Title>
</Modal.Header>
<Modal.Body>
Are you sure you want to leave the page without saving changes to the form?
<Row>
<Col xs={6}><Button block onClick={onHide}>Cancel</Button></Col>
<Col xs={6}><Button block onClick={moveForward}>OK</Button></Col>
</Row>
</Modal.Body>
</Modal>
</div>
)
}
}

Related

React Router v6 doesn't support usePrompt and useBlock anymore [duplicate]

I am basically trying to intercept route changes. Maybe something equivalent of vue's beforeEach in React Router v6 could be useful as React Router v.6 does not include usePrompt.
BEFORE each route change I want to do some logic - the logic might need to interrupt or even change the end route based on the result.
I have searched around but I really can't find something that solves this specific problem.
Thanks in advance.
Currently they have removed the usePrompt from the react-router v6.
I found a solution from ui.dev and added TypeScript support, and am now using that until the react-router will bring back the usePrompt/useBlocker hooks
import { History, Transition } from 'history';
import { useCallback, useContext, useEffect } from "react";
import { Navigator } from 'react-router';
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";
type ExtendNavigator = Navigator & Pick<History, "block">;
export function useBlocker(blocker: (tx: Transition) => void, when = true) {
const { navigator } = useContext(NavigationContext);
useEffect(() => {
if (!when) return;
const unblock = (navigator as ExtendNavigator).block((tx) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
export default function usePrompt(message: string, when = true) {
const blocker = useCallback((tx: Transition) => {
if (window.confirm(message)) tx.retry();
}, [message]);
useBlocker(blocker, when);
}
This can then be used in any view/component where you would like a "A you sure you want to leave?"-message displayed when the condition is true.
usePrompt("Do you want to leave?", isFormDirty());
Yes usePrompt and useBlock has been removed, but you can achieve same thing using history.block, here is the working example for blocking navigation using history.block with custom modal in React Router Dom V5
import { useHistory } from "react-router-dom";
import { UnregisterCallback } from "history";
...
type Prop = {
verify?: {
blockRoute?: (nextRoute: string) => boolean;
}
};
...
// in the component where you want to show confirmation modal on any nav change
const history = useHistory();
const unblock = useRef<UnregisterCallback>();
const onConfirmExit = () => {
/**
* if user confirms to exit, we can allow the navigation
*/
// Unblock the navigation.
unblock?.current?.();
// Proceed with the blocked navigation
goBack();
};
useEffect(() => {
/**
* Block navigation and register a callback that
* fires when a navigation attempt is blocked.
*/
unblock.current = history.block(({ pathname: to }) => {
/**
* Simply allow the transition to pass immediately,
* if user does not want to verify the navigate away action,
* or if user is allowed to navigate to next route without blocking.
*/
if (!verify || !verify.blockRoute?.(to)) return undefined;
/**
* Navigation was blocked! Let's show a confirmation dialog
* so the user can decide if they actually want to navigate
* away and discard changes they've made in the current page.
*/
showConfirmationModal();
// prevent navigation
return false;
});
// just in case theres an unmount we can unblock if it exists
return unblock.current;
}, [history]);
Here is a JS example of the react-route-dom v6 usePrompt if you're not using TS.
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
export function useBlocker( blocker, when = true ) {
const { navigator } = useContext( NavigationContext );
useEffect( () => {
if ( ! when ) return;
const unblock = navigator.block( ( tx ) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};
blocker( autoUnblockingTx );
} );
return unblock;
}, [ navigator, blocker, when ] );
}
export function usePrompt( message, when = true ) {
const blocker = useCallback(
( tx ) => {
// eslint-disable-next-line no-alert
if ( window.confirm( message ) ) tx.retry();
},
[ message ]
);
useBlocker( blocker, when );
}
Then the implementation would be...
const MyComponent = () => {
const formIsDirty = true; // Condition to trigger the prompt.
usePrompt( 'Leave screen?', formIsDirty );
return (
<div>Hello world</div>
);
};
Here's the article with the example

How would you use conditional hooks inside a React.Component class

Per documentation, Hooks cannot be used inside class components. But there are ways with higher order components: How can I use React hooks in React classic `class` component?. However this answer provided does not address the case of hooks that get called on function invocation. Take this simple Toast hook from: https://jossmac.github.io/react-toast-notifications/. I'd like to call the hook inside of a class of form:
```
class MyClass extends React.Component {
onTapButton = () => {
if(conditionTrue){
addToast('hello world', {
appearance: 'error',
autoDismiss: true,
})
}
}
render(){ ... }
}
```
There'd be no way of calling addToast without using const { addToast } = useToasts() in the class method, which would throw error.
You can use withToastManager HOC to archive that work
Here is an example
import React, { Component } from 'react';
import { withToastManager } from 'react-toast-notifications';
class ConnectivityListener extends Component {
state = { isOnline: window ? window.navigator.onLine : false };
// NOTE: add/remove event listeners omitted for brevity
onlineCallback = () => {
this.props.toastManager.remove(this.offlineToastId);
this.offlineToastId = null;
};
offlineCallback = id => {
this.offlineToastId = id;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
const { isOnline } = this.state;
if (prevState.isOnline !== isOnline) {
return { isOnline };
}
return null;
}
componentDidUpdate(props, state, snapshot) {
if (!snapshot) return;
const { toastManager } = props;
const { isOnline } = snapshot;
const content = (
<div>
<strong>{isOnline ? 'Online' : "Offline"}</strong>
<div>
{isOnline
? 'Editing is available again'
: 'Changes you make may not be saved'}
</div>
</div>
);
const callback = isOnline
? this.onlineCallback
: this.offlineCallback;
toastManager.add(content, {
appearance: 'info',
autoDismiss: isOnline,
}, callback);
}
render() {
return null;
}
}
export default withToastManager(ConnectivityListener);
For more information you can also find here

How to redirect to any url after login in redirect?

I want to redirect my user to any url that he types in after logging in
for example; my user types in the browser, http://localhost:3000/login/tickets,
If he has not logged in I would need the program to load the login page, and after login, the program redirects to this page, I can do it with a single page but I wanted it to be dynamic, something like this.
isAuthenticated()
? (
<Component {...props} />
) : <Redirect to=`/login?next=${this.props.location.search}` />
)}
soon this redirect would load the login page with the tag next
My solution was to do essentially what you describe. I made a HOC to wrap my route's component in if it requires that the user be logged in:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter, Redirect } from 'react-router-dom';
/**
* Higher-order component (HOC) to wrap restricted pages
*/
export default function LoggedInOnly(BaseComponent) {
class Restricted extends Component {
state = {};
static getDerivedStateFromProps(nextProps) {
const { history, location } = nextProps;
if (!nextProps.isLoggedIn) {
history.replace({ pathname: '/signin', search: `dest=${encodeURIComponent(location.pathname)}` });
}
return null;
}
render() {
const { location, staticContext } = this.props;
if (this.props.isLoggedIn) return <BaseComponent {...this.props} />;
const destinationURL = `/signin?dest=${encodeURIComponent(location.pathname)}`;
if (staticContext) staticContext.url = destinationURL;
return <Redirect to={destinationURL} />;
}
}
const mapStateToProps = state => ({
isLoggedIn: !!state.globalUserState.loggedInUserEmail,
});
return withRouter(connect(mapStateToProps)(Restricted));
}
I also set the url on the static context in my case so I can handle redirects appropriately in server side rendering. If you're not doing the same you can ignore that part.
For using it, though, I redirect after my SSR render function, like so:
if (context.url) {
console.log(`Redirecting to URL ${context.url}`);
return res.redirect(context.url);
}
A route using this would look like:
<Route path="/preferences" component={LoggedInOnly(SettingsView)} />
On my login page I grab the url parameters to see if there's a destination. If there is, I redirect there on login success.
I do this using query-string and the search component of the location:
const { destination } = queryString.parse(props.location.search);
The above assumes you're using withRouter to get the location info in the props.
On authentication success in the client I simply redirect to destination if it exists:
window.location.href = this.props.destination;
You could also use history.push or similar to accomplish the above.
In my case, as you can see, I'm using redux to track the logged in user state.
you need to update state by taking a variable and apply check wether it has been changed or not, if yes then redirect to desired page ,if not revert back. Since you have not posted your whole code .You can refer to this video for wider and clear perspective :
https://www.youtube.com/watch?v=zSt5G3s3OJI
You can accomplish what you need by doing some thing like this:
if(isAuthenticated)
this.props.history.push('/login', {lastPage: this.props.location.match})
and after user gets logged in you cant redirect him to passed param lastPage!
Another way is to store lastPage in redux and access it after user get logged in.
I had the same problem, i made a HOC to solve it.
import React from "react";
import { connect } from "react-redux";
import Login from "../../Auth/Login";
import { withRouter } from "react-router-dom";
import qs from "querystring";
const signInPath = "/signin";
const signUpPath = "/signup";
const forgotPassPath = "/forgot";
const resetPassPath = "/resetpassword";
const returlUrlPath = "returnUrl";
const allowedPaths = pathname =>
pathname === signInPath ||
pathname === signUpPath ||
pathname === forgotPassPath ||
pathname === resetPassPath;
const homePath = "/";
export default Component => {
class AuthComponent extends React.Component {
componentDidMount() {
this.checkAuthentication();
}
componentDidUpdate(nextProps) {
if (
nextProps.location.pathname !== this.props.location.pathname ||
this.props.loggedIn !== nextProps.loggedIn
) {
this.checkAuthentication();
}
}
checkAuthentication() {
const {
loggedIn,
history,
location: { pathname, search }
} = this.props;
if (!loggedIn) {
if (!allowedPaths(pathname)) {
const returlUrl =
pathname.length > 1
? `${returlUrlPath}=${pathname.replace("/", "")}`
: undefined;
history.replace({ pathname: signInPath, search: returlUrl });
}
} else if (search) {
const parsedSearch = qs.parse(search.replace("?", ""));
if (parsedSearch.returnUrl) {
history.replace({ pathname: parsedSearch.returnUrl });
} else {
history.replace({ pathname: homePath });
}
} else if (
history.location.pathname === signInPath ||
history.location.pathname === signUpPath
) {
history.replace({ pathname: homePath });
}
}
shouldRedirectToLogin() {
const {
location: { pathname }
} = this.props;
return (
!this.props.loggedIn &&
pathname !== signUpPath &&
pathname !== forgotPassPath &&
pathname !== resetPassPath
);
}
render() {
return this.shouldRedirectToLogin() ? (
<Login></Login>
) : (
<Component {...this.props}></Component>
);
}
}
return withRouter(
connect(({ user: { loggedIn } }) => {
return {
loggedIn
};
})(AuthComponent)
);
};
Thanks for all, after a lot of research i get only:
const params = (this.props.children.props.computedMatch.url);
return <Redirect to={`/login/?next=${params}`} />;

React-native: rendering view before fetching data from Storage

I am trying to render Signin component if user not logged in and if user logged in I am trying to render Home component. On Signin component set Storage 'isLIn' to 'true' On Signout [from home component] set Storage 'isLIn' to 'false' and Every time React-Native App opens checking Storage and Setting State as value of Storage.
Please look at code:
import React, { Component } from 'react';
import { AsyncStorage } from 'react-native';
import { Scene, Router } from 'react-native-router-flux';
import Login from './login_component';
import Home from './home_component';
var KEY = 'isLIn';
export default class main extends Component {
state = {
isLoggedIn: false
};
componentWillMount() {
this._loadInitialState().done();
}
_loadInitialState = async () => {
try {
let value = await AsyncStorage.getItem(KEY);
if (value !== null && value === 'true') {
this.setState({ isLoggedIn: true });
} else {
this.setState({ isLoggedIn: false });
}
} catch (error) {
console.error('Error:AsyncStorage:', error.message);
}
};
render() {
const _isIn = (this.state.isLoggedIn===true) ? true : false;
return (
<Router>
<Scene key="root" hideNavBar hideTabBar>
<Scene key="Signin" component={Login} title="Signin" initial={!_isIn} />
<Scene key="Home" component={Home} title="Home" initial={_isIn}/>
</Scene>
</Router>
);
}
}
I don't know why but view render first before Storage gets value. According to lifecycle of react-native render() execute only after componentWillMount() as React_Doc says.
I am using AsyncStorage to get set and remove Storage and also using React-Native-Router-Flux for routing.
I have tried solutions:
forceUpdate
Solution1
Since what you are doing is asynchronous you can not tell the lifecycle to wait for it. But React provides states and these you can use e.g.
state = {
isLoggedIn: false
isLoading: true
};
And set the state in the async
_loadInitialState = async () => {
try {
let value = await AsyncStorage.getItem(KEY);
if (value !== null && value === 'true') {
this.setState({ isLoggedIn: true, isLoading: false });
} else {
this.setState({ isLoggedIn: false, isLoading: false });
}
} catch (error) {
console.error('Error:AsyncStorage:', error.message);
}
};
And then in your render method you can place a placeholder until your asynctask is finished
render() {
if(this.state.isLoading) return <div> Loading... </div>
else return...
}
Invoking setState in componentWillMount does NOT trigger a re-rendering. componentWillMount runs after state has been set and before the view has been re-rendered. From React Native Docs:
"componentWillMount() is invoked immediately before mounting occurs. It is called before render(), therefore setting state in this method will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method." - https://facebook.github.io/react/docs/react-component.html#componentwillmount
Instead, you should call _loadInitialState in componentWillReceiveProps()

react-router setRouteLeaveHook still updating URL

I'm using react 15.3.1, with react-router 2.4.1 and react-router-redux 4.0.5:
When I trap the routing change with:
this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
);
private routerWillLeave = () => {
if (this.state.editing)
return 'You may have unsaved changes. Are you sure you want to leave?'
};
... I do get my this.routerWillLeave method called, but the URL in the browser still changes, so even if the user stays on the page by deciding not to leave the page, the URL is now wrong. Ideas?
export default class extends Component {
static contextTypes = {
router: React.PropTypes.object.isRequired
}
state = {
editing: true
}
componentDidMount() {
this.context.router.setRouteLeaveHook(this.props.route, () => {
if (this.state.editing) {
return false;
// At here you can give a confirm dialog, return true when confirm true
}else {
return true;
}
})
}
}
And if your react-route >2.4, you can also use withRouter to wrap your component, this's may be better!
import React, {Component} from 'react';
import {render} from 'react-dom';
import {withRouter} from 'react-router';
export default withRouter(class extends Component {
state = {
unsaved: true
}
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, () => {
if (this.state.unsaved) {
return false;
// At here you can give a confirm dialog, return true when confirm true
}else {
return true;
}
})
}
render() {
return (
<div>
<h2>About</h2>
{this.props.children || "This is outbox default!"}
</div>
)
}
})

Categories