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

118

Я хочу, чтобы мое приложение ReactJS уведомляло пользователя при переходе с определенной страницы. В частности, всплывающее сообщение, напоминающее ему / ей о действии:

«Изменения сохранены, но еще не опубликованы. Сделать это сейчас?»

Должен ли я запускать это react-routerглобально или это можно сделать на странице / компоненте реакции?

Я не нашел ничего по последнему, и я бы предпочел избегать первого. Если, конечно, это не норма, но это заставляет меня задаться вопросом, как это сделать, не добавляя код на каждую другую возможную страницу, на которую может перейти пользователь.

Любые идеи приветствуются, спасибо!

Барри Стаес
источник
3
Я не знаю, если это то, что вы ищете, но вы можете что-то сделать. вот componentWillUnmount() { if (confirm('Changes are saved, but not published yet. Do that now?')) { // publish and go away from a specific page } else { // do nothing and go away from a specific page } }так, чтобы вы могли вызвать свою функцию публикации перед выходом со страницы
Рико Бергер

Ответы:

156

react-routerv4 представляет новый способ блокировки навигации с помощью 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 имеет различную поддержку браузерами.

jcady
источник
9
Однако это приводит к появлению двух очень разных предупреждений.
XanderStrike
3
@XanderStrike Вы можете попытаться стилизовать подсказку, имитирующую оповещение браузера по умолчанию. К сожалению, нет способа стилизовать onberforeunloadпредупреждение.
jcady
Есть ли способ показать тот же компонент подсказки, когда пользователь, например, нажимает кнопку ОТМЕНА?
Рене Энрикес
1
@ReneEnriquez react-routerне поддерживает это из коробки (при условии, что кнопка отмены не вызывает никаких изменений маршрута). Однако вы можете создать свой собственный модальный файл, чтобы имитировать поведение.
jcady
6
Если вы в конечном итоге используете onbeforeunload , вы захотите очистить его, когда ваш компонент отключится. componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch
40

В реактивном маршрутизаторе 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 from'react-router.

Согласно документации

Подсказка

Используется для запроса пользователю перед уходом со страницы. Когда ваше приложение переходит в состояние, которое должно препятствовать переходу пользователя (например, форма наполовину заполнена), визуализируйте файл <Prompt>.

import { Prompt } from 'react-router'

<Prompt
  when={formIsHalfFilledOut}
  message="Are you sure you want to leave?"
/>

сообщение: строка

Сообщение, которое будет подсказывать пользователю, когда он пытается уйти.

<Prompt message="Are you sure you want to leave?"/>

сообщение: func

Будет вызываться со следующим местоположением и действием, к которому пользователь пытается перейти. Верните строку, чтобы отобразить подсказку пользователю, или значение true, чтобы разрешить переход.

<Prompt message={location => (
  `Are you sure you want to go to ${location.pathname}?`
)}/>

когда: bool

Вместо условного рендеринга <Prompt>за охранником вы всегда можете рендерить его, но пропустить when={true}или when={false}запретить или разрешить навигацию соответственно.

В вашем методе рендеринга вам просто нужно добавить это, как указано в документации, в соответствии с вашими потребностями.

ОБНОВИТЬ:

Если вы хотите иметь настраиваемое действие, которое будет выполняться, когда пользователь покидает страницу, вы можете использовать настраиваемую историю и настроить свой маршрутизатор, например

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
   }
}
Шубхам Хатри
источник
1
Даже если вы используете <Prompt>URL-адрес, он будет изменен, когда вы нажмете Отмена в приглашении. Связанная проблема: github.com/ReactTraining/react-router/issues/5405
Sahan Serasinghe
1
Я вижу, этот вопрос все еще довольно популярен. Поскольку основные ответы касаются старых устаревших версий, я установил этот новый ответ как принятый. Ссылки на старую документацию (и руководства по обновлению) теперь доступны на github.com/ReactTraining/react-router
Barry Staes,
1
onLeave запускается ПОСЛЕ подтверждения изменения маршрута. Не могли бы вы подробнее рассказать, как отменить навигацию в onLeave?
Schlingel
23

Для react-router2.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
Что делает обратный вызов RouteLeaveHook? Предлагает ли он пользователю использовать встроенное модальное окно? Что делать, если вам нужен собственный модальный
Learner
Как @Learner: как это сделать с помощью настраиваемого окна подтверждения, такого как vex или SweetAlert, или другого неблокирующего диалога?
olefrank
У меня следующая ошибка: - TypeError: не удается прочитать свойство « id » неопределенного значения. Любое предложение
Мустафа Мамун
@MustafaMamun Это кажется несвязанной проблемой, и вы в основном захотите создать новый вопрос, объясняющий детали.
snymkpr
У меня возникла проблема при попытке использовать решение. Компонент должен быть напрямую подключен к маршрутизатору, чтобы это решение работало. Я нашел там лучший ответ stackoverflow.com/questions/39103684/…
Мустафа Мамун
4

Для react-routerv3.x

У меня была такая же проблема, когда мне требовалось подтверждающее сообщение для любого несохраненного изменения на странице. В моем случае я использовал React Router v3 , поэтому я не мог использовать <Prompt />, который был введен в React Router v4 .

Я обработал «нажатие кнопки возврата» и «случайное нажатие ссылки» с помощью комбинации setRouteLeaveHookи history.pushState(), а также обработал «кнопку перезагрузки» с помощью onbeforeunloadобработчика событий.

setRouteLeaveHook ( документ ) и history.pushState ( документ )

  • Использования только 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 ( док )

  • Он используется для обработки кнопки случайной перезагрузки

    window.onbeforeunload = this.handleOnBeforeUnload;
    
    ...
    
    handleOnBeforeUnload = e => {
      const message = 'Are you sure?';
      e.returnValue = message;
      return message;
    }

Ниже представлен полный компонент, который я написал

  • обратите внимание, что withRouter используется дляthis.props.router .
  • обратите внимание, что this.props.routeпередается от вызывающего компонента
  • обратите внимание, что currentStateпередается как опора, чтобы иметь начальное состояние и проверять любые изменения

    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);

Пожалуйста, дайте мне знать, если возникнут вопросы :)

Сан Юн Парк
источник
откуда именно this.formStartEle исходит?
Gaurav
2

Для react-routerv0.13.x с reactv0.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-router1.0.0-rc1 с reactv0.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?'
    },

    // ...

  })

Вещи. может измениться до финальной версии.

Барри Стаес
источник
1

Вы можете использовать эту подсказку.

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;
Джаскаран Сингх
источник
1

Использование history.listen

Например, как показано ниже:

В вашем компоненте

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}
Дебабрата Гош
источник
5
Привет, добро пожаловать в Stack Overflow. Отвечая на вопрос, на который уже есть много ответов, не забудьте добавить дополнительную информацию о том, почему ответ, который вы предоставляете, является существенным, а не просто повторением того, что уже было проверено исходным плакатом. Это особенно важно в ответах «только код», таких как тот, который вы предоставили.
chb