Обнаружение страницы, удаляющей пользователя с помощью ретранслятора

Я хочу, чтобы мое приложение 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 есть несколько вариантов

  1. Добавить функцию onLeave для Route
 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
  1. Используйте функцию 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;