Обнаружение страницы, удаляющей пользователя с помощью ретранслятора
Я хочу, чтобы мое приложение ReactJS уведомило пользователя при навигации по определенной странице. В частности, всплывающее сообщение, напоминающее ему/ей, чтобы выполнить действие:
"Изменения сохранены, но еще не опубликованы. Сделайте это сейчас?"
Должен ли я запускать это на react-router
глобально, или это что-то, что можно сделать изнутри реагирующей страницы/компонента?
Я ничего не нашел на последнем, и я предпочел бы избежать первого. Если это не норма, конечно, но это заставляет меня задаться вопросом, как это сделать, не добавляя код на любую другую возможную страницу, к которой пользователь может перейти.
Любые идеи приветствуются, спасибо!
Ответы
Ответ 1
react-router
v4 представляет новый способ блокировки навигации с использованием Prompt
. Просто добавьте это к компоненту, который вы хотите заблокировать:
import { Prompt } from 'react-router'
const MyComponent = () => (
<React.Fragment>
<Prompt
when={shouldBlockNavigation}
message='You have unsaved changes, are you sure you want to leave?'
/>
{/* Component JSX */}
</React.Fragment>
)
Это заблокирует любую маршрутизацию, но не обновление страницы или закрытие. чтобы заблокировать это, вам нужно добавить это (при необходимости обновить с соответствующим жизненным циклом React):
componentDidUpdate = () => {
if (shouldBlockNavigation) {
window.onbeforeunload = () => true
} else {
window.onbeforeunload = undefined
}
}
onbeforeunload имеет различную поддержку браузерами.
Ответ 2
В реакции-маршрутизаторе v2.4.0
или выше и до v4
есть несколько вариантов
- Добавить функцию
onLeave
для Route
<Route
path="/home"
onEnter={ auth }
onLeave={ showConfirm }
component={ Home }
>
- Используйте функцию
setRouteLeaveHook
для componentDidMount
Вы можете предотвратить переход или запросить пользователя, прежде чем покинуть маршрут с помощью крюка отпуска.
const Home = withRouter(
React.createClass({
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave)
},
routerWillLeave(nextLocation) {
// return false to prevent a transition w/o prompting the user,
// or return a string to allow the user to decide:
// return 'null' or nothing to let other hooks to be executed
//
// NOTE: if you return true, other hooks will not be executed!
if (!this.state.isSaved)
return 'Your work is not saved! Are you sure you want to leave?'
},
// ...
})
)
Обратите внимание, что в этом примере используется компонент высшего порядка withRouter
, представленный в v2.4.0.
Однако это решение не совсем идеально работает при изменении маршрута в URL-адресе вручную.
В том смысле, что
- мы видим подтверждение - ок
- содержимое страницы не перезагружается - хорошо
- URL не меняется - не в порядке
Для react-router v4
с использованием подсказки или пользовательской истории:
Однако в react-router v4
его проще реализовать с помощью Prompt
из'act-router
Согласно документации
Prompt
Используется для запроса пользователя перед переходом со страницы. Когда ваш приложение переходит в состояние, которое должно препятствовать пользователю переходя прочь (как будто форма наполовину заполнена), визуализируйте <Prompt>
.
import { Prompt } from 'react-router'
<Prompt
when={formIsHalfFilledOut}
message="Are you sure you want to leave?"
/>
message: string
Сообщение, с помощью которого пользователь должен попытаться уйти.
<Prompt message="Are you sure you want to leave?"/>
message: func
Будет вызван со следующим местоположением и действием пользователя пытаясь перейти к. Вернуть строку, чтобы показать подсказку пользователь или true, чтобы разрешить переход.
<Prompt message={location => (
'Are you sure you want to go to ${location.pathname}?'
)}/>
when: bool
Вместо условного рендеринга <Prompt>
за охранником, вы всегда может сделать это, но передать when={true}
или when={false}
запретить или разрешить навигацию соответственно.
В вашем методе рендеринга вам просто нужно добавить это, как указано в документации, в соответствии с вашими потребностями.
UPDATE:
В случае, если вы хотите иметь настраиваемое действие, которое необходимо выполнить, когда вы покидаете страницу, вы можете использовать настраиваемую историю и настроить свой маршрутизатор, например,
history.js
import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()
...
import { history } from 'path/to/history';
<Router history={history}>
<App/>
</Router>
и затем в своем компоненте вы можете использовать history.block
как
import { history } from 'path/to/history';
class MyComponent extends React.Component {
componentDidMount() {
this.unblock = history.block(targetLocation => {
// take your action here
return false;
});
}
componentWillUnmount() {
this.unblock();
}
render() {
//component render here
}
}
Ответ 3
Для react-router
2.4.0 +
ПРИМЕЧАНИЕ. Желательно перенести весь свой код на react-router
, чтобы получить все новые лакомства.
Как рекомендовано в документации реакция-маршрутизатор:
Следует использовать компонент withRouter
более высокого порядка:
Мы считаем, что этот новый HoC лучше и проще, и будет использовать его в документацию и примеры, но это не переключатель.
Как пример ES6 из документации:
import React from 'react'
import { withRouter } from 'react-router'
const Page = React.createClass({
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, () => {
if (this.state.unsaved)
return 'You have unsaved information, are you sure you want to leave this page?'
})
}
render() {
return <div>Stuff</div>
}
})
export default withRouter(Page)
Ответ 4
Для react-router
v3.x
У меня была такая же проблема, когда мне понадобилось подтверждение для любого несохраненного изменения на странице. В моем случае я использовал React Router v3, поэтому я не мог использовать <Prompt/>
, который был введен с React Router v4.
Я обработал "обратную кнопку" и "случайную ссылку" с комбинацией setRouteLeaveHook
и history.pushState()
и обработал "перезагрузку" с onbeforeunload
обработчика события onbeforeunload
.
setRouteLeaveHook (doc) & history.pushState (doc)
-
Использовать только setRouteLeaveHook было недостаточно. По какой-то причине URL был изменен, хотя страница осталась прежней, когда была нажата кнопка "Назад".
// setRouteLeaveHook returns the unregister method
this.unregisterRouteHook = this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
);
...
routerWillLeave = nextLocation => {
// Using native 'confirm' method to show confirmation message
const result = confirm('Unsaved work will be lost');
if (result) {
// navigation confirmed
return true;
} else {
// navigation canceled, pushing the previous path
window.history.pushState(null, null, this.props.route.path);
return false;
}
};
onbeforeunload (doc)
-
Он используется для обработки кнопки "случайной перезагрузки"
window.onbeforeunload = this.handleOnBeforeUnload;
...
handleOnBeforeUnload = e => {
const message = 'Are you sure?';
e.returnValue = message;
return message;
}
Ниже приведен полный компонент, который я написал
- обратите внимание, что withRouter используется, чтобы
this.props.router
. - обратите внимание, что
this.props.route
передается от вызывающего компонента -
обратите внимание, что currentState
передается как prop, чтобы иметь начальное состояние и проверять любые изменения
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { withRouter } from 'react-router';
import Component from '../Component';
import styles from './PreventRouteChange.css';
class PreventRouteChange extends Component {
constructor(props) {
super(props);
this.state = {
// initialize the initial state to check any change
initialState: _.cloneDeep(props.currentState),
hookMounted: false
};
}
componentDidUpdate() {
// I used the library called 'lodash'
// but you can use your own way to check any unsaved changed
const unsaved = !_.isEqual(
this.state.initialState,
this.props.currentState
);
if (!unsaved && this.state.hookMounted) {
// unregister hooks
this.setState({ hookMounted: false });
this.unregisterRouteHook();
window.onbeforeunload = null;
} else if (unsaved && !this.state.hookMounted) {
// register hooks
this.setState({ hookMounted: true });
this.unregisterRouteHook = this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
);
window.onbeforeunload = this.handleOnBeforeUnload;
}
}
componentWillUnmount() {
// unregister onbeforeunload event handler
window.onbeforeunload = null;
}
handleOnBeforeUnload = e => {
const message = 'Are you sure?';
e.returnValue = message;
return message;
};
routerWillLeave = nextLocation => {
const result = confirm('Unsaved work will be lost');
if (result) {
return true;
} else {
window.history.pushState(null, null, this.props.route.path);
if (this.formStartEle) {
this.moveTo.move(this.formStartEle);
}
return false;
}
};
render() {
return (
<div>
{this.props.children}
</div>
);
}
}
PreventRouteChange.propTypes = propTypes;
export default withRouter(PreventRouteChange);
Пожалуйста, дайте мне знать, если есть какие-либо вопросы :)
Ответ 5
Для react-router
v0.13.x с react
v0.13.x:
это возможно с помощью статических методов willTransitionTo()
и willTransitionFrom()
. Для более новых версий см. Мой другой ответ ниже.
Из документация по реакции-маршрутизатора:
Вы можете определить некоторые статические методы для ваших обработчиков маршрутов, которые будут вызываться во время переходов маршрута.
willTransitionTo(transition, params, query, callback)
Вызывается, когда обработчик собирается выполнить рендеринг, что дает вам возможность прервать или перенаправить переход. Вы можете приостановить переход, пока выполняете какую-то асинхронную работу и вызываете обратный вызов (ошибку), когда закончите, или опустите обратный вызов в списке аргументов, и он будет вызван для вас.
willTransitionFrom(transition, component, callback)
Вызывается при переходе активного маршрута, что дает вам возможность прервать переход. Компонент является текущим компонентом, вам, вероятно, понадобится его проверить его состояние, чтобы решить, хотите ли вы разрешить переход (например, поля формы).
Пример
var Settings = React.createClass({
statics: {
willTransitionTo: function (transition, params, query, callback) {
auth.isLoggedIn((isLoggedIn) => {
transition.abort();
callback();
});
},
willTransitionFrom: function (transition, component) {
if (component.formHasUnsavedData()) {
if (!confirm('You have unsaved information,'+
'are you sure you want to leave this page?')) {
transition.abort();
}
}
}
}
//...
});
Для react-router
1.0.0-rc1 с react
v0.14.x или новее:
это возможно с помощью крюка жизненного цикла routerWillLeave
. Для более старых версий см. Мой ответ выше.
В документации реакция-маршрутизатор:
Чтобы установить этот крючок, используйте комбинацию Lifecycle в одном из компонентов маршрута.
import { Lifecycle } from 'react-router'
const Home = React.createClass({
// Assuming Home is a route component, it may use the
// Lifecycle mixin to get a routerWillLeave method.
mixins: [ Lifecycle ],
routerWillLeave(nextLocation) {
if (!this.state.isSaved)
return 'Your work is not saved! Are you sure you want to leave?'
},
// ...
})
вещи. может измениться до окончательного релиза.
Ответ 6
Использование history.listen
Например, как показано ниже:
В вашем компоненте
componentWillMount() {
this.props.history.listen(() => {
// Detecting, user has changed URL
console.info(this.props.history.location.pathname);
});
}
Ответ 7
Вы можете использовать эту подсказку.
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link, Prompt } from "react-router-dom";
function PreventingTransitionsExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Form</Link>
</li>
<li>
<Link to="/one">One</Link>
</li>
<li>
<Link to="/two">Two</Link>
</li>
</ul>
<Route path="/" exact component={Form} />
<Route path="/one" render={() => <h3>One</h3>} />
<Route path="/two" render={() => <h3>Two</h3>} />
</div>
</Router>
);
}
class Form extends Component {
state = { isBlocking: false };
render() {
let { isBlocking } = this.state;
return (
<form
onSubmit={event => {
event.preventDefault();
event.target.reset();
this.setState({
isBlocking: false
});
}}
>
<Prompt
when={isBlocking}
message={location =>
'Are you sure you want to go to ${location.pathname}'
}
/>
<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 => {
this.setState({
isBlocking: event.target.value.length > 0
});
}}
/>
</p>
<p>
<button>Submit to stop blocking</button>
</p>
</form>
);
}
}
export default PreventingTransitionsExample;