การตรวจจับผู้ใช้ออกจากหน้าด้วย react-router


120

ฉันต้องการให้แอป ReactJS ของฉันแจ้งเตือนผู้ใช้เมื่อออกจากหน้าใดหน้าหนึ่ง โดยเฉพาะข้อความป๊อปอัปที่เตือนให้เขา / เธอดำเนินการ:

"บันทึกการเปลี่ยนแปลงแล้ว แต่ยังไม่เผยแพร่ทำตอนนี้เลยไหม"

ฉันควรทริกเกอร์สิ่งนี้ในreact-routerทั่วโลกหรือนี่คือสิ่งที่สามารถทำได้จากภายในหน้าปฏิกิริยา / ส่วนประกอบ?

ฉันไม่พบอะไรเลยในตอนหลังและฉันควรหลีกเลี่ยงสิ่งแรก เว้นแต่จะเป็นบรรทัดฐาน แต่นั่นทำให้ฉันสงสัยว่าจะทำสิ่งนั้นได้อย่างไรโดยไม่ต้องเพิ่มโค้ดในทุกหน้าที่เป็นไปได้ที่ผู้ใช้สามารถไปที่ ..

ยินดีต้อนรับข้อมูลเชิงลึกใด ๆ ขอบคุณ!


3
ฉันไม่รู้ว่านี่คือสิ่งที่คุณกำลังค้นหา แต่คุณทำได้ sth เช่นนี้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 } }เพื่อให้คุณสามารถเรียกใช้ฟังก์ชันการเผยแพร่ของคุณได้หรือออกจากหน้า
Rico Berger

คำตอบ:


159

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

การดำเนินการนี้จะบล็อกการกำหนดเส้นทางใด ๆ แต่จะไม่รีเฟรชหรือปิดหน้า ในการบล็อกคุณจะต้องเพิ่มสิ่งนี้ (อัปเดตตามความจำเป็นด้วยวงจรชีวิตของปฏิกิริยาที่เหมาะสม):

componentDidUpdate = () => {
  if (shouldBlockNavigation) {
    window.onbeforeunload = () => true
  } else {
    window.onbeforeunload = undefined
  }
}

onbeforeunloadมีการสนับสนุนที่หลากหลายโดยเบราว์เซอร์


10
ซึ่งส่งผลให้เกิดการแจ้งเตือนที่ดูแตกต่างกันมากสองรายการ
XanderStrike

3
@XanderStrike คุณสามารถลองจัดรูปแบบการแจ้งเตือนเพื่อเลียนแบบการแจ้งเตือนเริ่มต้นของเบราว์เซอร์ น่าเสียดายที่ไม่มีวิธีจัดรูปแบบการonberforeunloadแจ้งเตือน
jcady

มีวิธีใดบ้างในการแสดงคอมโพเนนต์พรอมต์เดียวกันเมื่อผู้ใช้คลิกที่ปุ่มยกเลิกเช่น?
Rene Enriquez

1
@ReneEnriquez react-routerไม่รองรับสิ่งนั้นนอกกรอบ (สมมติว่าปุ่มยกเลิกไม่ได้ทริกเกอร์การเปลี่ยนแปลงเส้นทางใด ๆ ) คุณสามารถสร้างโมดอลของคุณเองเพื่อเลียนแบบพฤติกรรมได้
jcady

7
หากคุณสิ้นสุดการใช้onbeforeunload งานคุณจะต้องทำความสะอาดเมื่อส่วนประกอบของคุณเลิกต่อเชื่อม componentWillUnmount() { window.onbeforeunload = null; }
cdeutsch

40

ใน react-router 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ใช้ Prompt หรือประวัติแบบกำหนดเอง:

อย่างไรก็ตามในreact-router v4การใช้งานค่อนข้างง่ายกว่าด้วยความช่วยเหลือของPrompt React-router

ตามเอกสารประกอบ

พร้อมท์

ใช้เพื่อแจ้งผู้ใช้ก่อนที่จะออกจากหน้า เมื่อแอปพลิเคชันของคุณเข้าสู่สถานะที่ควรป้องกันไม่ให้ผู้ใช้นำทางออกไป (เช่นกรอกแบบฟอร์มครึ่งหนึ่ง) ให้แสดงผล a <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

จะถูกเรียกด้วยตำแหน่งถัดไปและการดำเนินการที่ผู้ใช้พยายามนำทางไป ส่งคืนสตริงเพื่อแสดงข้อความแจ้งแก่ผู้ใช้หรือจริงเพื่ออนุญาตการเปลี่ยนแปลง

<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

22

สำหรับreact-router2.4.0+

หมายเหตุ : ขอแนะนำให้ย้ายรหัสทั้งหมดของคุณเป็นรุ่นล่าสุดreact-routerเพื่อรับสินค้าใหม่ทั้งหมด

ตามที่แนะนำในเอกสาร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 ทำอะไร? มันแจ้งผู้ใช้โดยใช้โมดอลในตัวหรือไม่? จะเป็นอย่างไรถ้าคุณต้องการโมดอลที่กำหนดเอง
ผู้เรียน

Like @Learner: จะทำอย่างไรกับกล่องยืนยันที่กำหนดเองเช่น vex หรือ SweetAlert หรือกล่องโต้ตอบอื่น ๆ ที่ไม่ปิดกั้น
olefrank

ฉันมีข้อผิดพลาดต่อไปนี้: - TypeError: ไม่สามารถอ่านคุณสมบัติ ' id ' ของ undefined ข้อเสนอแนะใด ๆ
มุสตาฟามามุน

@MustafaMamun ดูเหมือนว่าจะเป็นปัญหาที่ไม่เกี่ยวข้องและส่วนใหญ่คุณจะต้องการสร้างคำถามใหม่เพื่ออธิบายรายละเอียด
snymkpr

ฉันประสบปัญหาขณะพยายามใช้วิธีแก้ปัญหา ส่วนประกอบจะต้องเชื่อมต่อโดยตรงกับเราเตอร์จากนั้นโซลูชันนี้ใช้งานได้ ฉันพบคำตอบที่ดีกว่าที่นั่นstackoverflow.com/questions/39103684/…
มุสตาฟามามุน

4

สำหรับreact-routerv3.x

ฉันประสบปัญหาเดียวกันกับที่ฉันต้องการข้อความยืนยันสำหรับการเปลี่ยนแปลงที่ยังไม่ได้บันทึกในหน้านี้ ในกรณีของฉันฉันถูกใช้ตอบสนอง Router v3ดังนั้นผมจึงไม่สามารถใช้<Prompt />ซึ่งเป็นที่รู้จักจากการตอบสนอง Router v4

ฉันจัดการ 'การคลิกปุ่มย้อนกลับ' และ 'การคลิกลิงก์โดยไม่ได้ตั้งใจ' ด้วยการรวมsetRouteLeaveHookและhistory.pushState()และจัดการ 'ปุ่มโหลดซ้ำ' ด้วย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;
    }

ด้านล่างนี้คือส่วนประกอบทั้งหมดที่ฉันเขียนไว้

  • ทราบว่าwithRouterthis.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()แบบคงที่ สำหรับเวอร์ชันที่ใหม่กว่าโปรดดูคำตอบอื่น ๆ ของฉันด้านล่าง

จากเอกสารreact-router :

คุณสามารถกำหนดวิธีการแบบคงที่บนตัวจัดการเส้นทางของคุณซึ่งจะถูกเรียกระหว่างการเปลี่ยนเส้นทาง

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- reactrc1 ที่มีv0.14.x หรือใหม่กว่า:

สิ่งนี้ควรเป็นไปได้ด้วยไฟล์ routerWillLeaveขอเกี่ยววงจรชีวิต สำหรับเวอร์ชันเก่าโปรดดูคำตอบของฉันด้านบน

จาก react-router :

ในการติดตั้งขอเกี่ยวนี้ให้ใช้ Lifecycle mixin ในส่วนประกอบเส้นทางของคุณ

  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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.