React Router v3实现返回/刷新未保存数据确认弹窗方案咨询
Got it, let's work through this problem since you're stuck on React Router v3 and can't use the v4+ <Prompt> component. First, let's break down why your existing approaches had issues, then implement a reliable solution that covers all scenarios: browser back/forward, refresh, and client-side route jumps.
Why Your Initial Approaches Failed
setRouteLeaveHookIssue:
When you returned a string from the hook, React Router v3 triggers the browser's default confirm dialog—but when the user clicks "Cancel", the router doesn't properly revert the URL change (this is a known quirk in v3). Also, you weren't cleaning up the hook whenisDirtybecame false, which could lead to false positives.beforeunload+popstateIssue:beforeunloadworks for refreshes/tab closes, but it doesn't trigger for client-side route changes (since those don't reload the page).popstatefires after the navigation has already happened, soe.preventDefault()does nothing to block it.
The Reliable Combined Solution
We'll combine setRouteLeaveHook (for client-side route navigation) with beforeunload (for refreshes/tab closes), and fix the URL mismatch issue with proper state management.
Step 1: Full Component Implementation
import React from 'react'; import { withRouter } from 'react-router'; class UnsavedForm extends React.Component { state = { isDirty: false, // Track if user confirmed leaving to avoid looped prompts confirmedLeave: false }; componentDidMount() { this.setupLeaveHook(); window.addEventListener('beforeunload', this.handleBeforeUnload); } componentDidUpdate(prevProps, prevState) { // Re-setup hook only when isDirty state changes if (prevState.isDirty !== this.state.isDirty) { this.setupLeaveHook(); } } componentWillUnmount() { // Clean up hook and event listeners to prevent memory leaks if (this.leaveHook) this.leaveHook(); window.removeEventListener('beforeunload', this.handleBeforeUnload); } setupLeaveHook() { const { router, route } = this.props; // Remove existing hook first to avoid duplicate bindings if (this.leaveHook) this.leaveHook(); if (this.state.isDirty && !this.state.confirmedLeave) { this.leaveHook = router.setRouteLeaveHook(route, (nextLocation) => { const shouldLeave = window.confirm('Unsaved data will be lost. Are you sure you want to leave?'); if (shouldLeave) { // Mark as confirmed so we don't prompt again during this navigation this.setState({ confirmedLeave: true }); // Manually trigger the navigation since we're blocking the default router.push(nextLocation.pathname); } // Return false to block the default router navigation return false; }); } } handleBeforeUnload = (e) => { if (this.state.isDirty) { const warningMsg = 'Unsaved data will be lost. Are you sure you want to leave?'; e.preventDefault(); e.returnValue = warningMsg; // Required for some browsers return warningMsg; } }; // Update isDirty when form changes, and reset confirmedLeave state handleFormChange = () => { this.setState({ isDirty: true, confirmedLeave: false }); }; // Reset isDirty after saving handleSave = () => { // Add your form save logic here this.setState({ isDirty: false }); }; render() { return ( <form> <input type="text" onChange={this.handleFormChange} placeholder="Enter some text..." /> <button onClick={this.handleSave} disabled={!this.state.isDirty}> Save Changes </button> </form> ); } } // Wrap with withRouter to access router props export default withRouter(UnsavedForm);
Step 2: Key Fixes & Explanations
- Cleanup Hooks/Listeners: We remove the route leave hook and
beforeunloadlistener when the component unmounts to prevent memory leaks and false prompts in other components. - Controlled Navigation: Instead of relying on the router's default behavior, we manually trigger navigation only when the user confirms leaving—this fixes the URL mismatch issue you saw earlier.
- State Tracking: The
confirmedLeaveflag prevents the prompt from re-firing during the same navigation flow. - Full Coverage:
setRouteLeaveHookhandles browser back/forward buttons and client-side route links.beforeunloadhandles page refreshes and tab/window closes.
内容的提问来源于stack exchange,提问作者Sang Yun Park

