@mehmetbarisyaman/react-navigation-guard
v1.0.7
Published
A React hook for preventing navigation when there are unsaved changes, with support for browser refresh and route changes. Compatible with React 19. Requires React Router.
Maintainers
Readme
React Navigation Guard
A React hook that prevents navigation when there are unsaved changes, providing a seamless user experience by showing confirmation dialogs before leaving pages with unsaved data.
Features
- 🛡️ Prevent accidental navigation away from pages with unsaved changes
- 🔄 Handle browser refresh attempts (F5, Ctrl+R)
- 🚪 Intercept beforeunload events for external navigation
- ⚡ TypeScript support with full type definitions
- 🎯 React Router integration with programmatic navigation control
- 🎨 Customizable callbacks for handling navigation attempts
Installation
npm install @mehmetbarisyaman/react-navigation-guardyarn add @mehmetbarisyaman/react-navigation-guardpnpm add @mehmetbarisyaman/react-navigation-guardQuick Start
// App.tsx - Setup Router
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import MyForm from './MyForm';
function App() {
return (
<BrowserRouter>
<div className="App">
<MyForm />
</div>
</BrowserRouter>
);
}
export default App;
// MyForm.tsx - Use the hook
import React, { useState } from 'react';
import { useNavigationGuard } from '@mehmetbarisyaman/react-navigation-guard';
function MyForm() {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const { allowPendingNavigation, cancelPendingNavigation } = useNavigationGuard({
shouldBlock: () => hasUnsavedChanges,
onNavigationAttempt: () => {
if (window.confirm('You have unsaved changes. Leave anyway?')) {
allowPendingNavigation();
} else {
cancelPendingNavigation();
}
}
});
return (
<div>
<input onChange={(e) => setHasUnsavedChanges(e.target.value !== '')} />
<button onClick={() => setHasUnsavedChanges(false)}>Save</button>
</div>
);
}
export default MyForm;Prerequisites
This package requires:
- React 16.8.0 or higher (for hooks support) - ✅ React 19 compatible
- react-router-dom 6.0.0 or higher - ⚠️ REQUIRED: Your component must be wrapped in a Router component
Important: This hook uses
useNavigatefrom react-router-dom, so your component must be inside a<Router>,<BrowserRouter>, or<HashRouter>component.
Basic Usage
// App.tsx - Root component with Router setup
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import MyForm from './MyForm';
import OtherPage from './OtherPage';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Form</Link> | <Link to="/other">Other Page</Link>
</nav>
<Routes>
<Route path="/" element={<MyForm />} />
<Route path="/other" element={<OtherPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
// MyForm.tsx - Component using the navigation guard
import React, { useState } from 'react';
import { useNavigationGuard } from '@mehmetbarisyaman/react-navigation-guard';
function MyForm() {
const [formData, setFormData] = useState('');
const [originalData, setOriginalData] = useState('');
const [showModal, setShowModal] = useState(false);
// Determine if there are unsaved changes
const hasUnsavedChanges = formData !== originalData;
const {
allowPendingNavigation,
cancelPendingNavigation
} = useNavigationGuard({
shouldBlock: () => hasUnsavedChanges,
onNavigationAttempt: (targetUrl, isReload) => {
setShowModal(true);
}
});
const handleSave = () => {
// Save logic here
setOriginalData(formData);
};
const handleConfirmLeave = () => {
setShowModal(false);
allowPendingNavigation();
};
const handleCancelLeave = () => {
setShowModal(false);
cancelPendingNavigation();
};
return (
<div>
<h2>Protected Form</h2>
<textarea
value={formData}
onChange={(e) => setFormData(e.target.value)}
placeholder="Enter some data..."
rows={4}
cols={50}
/>
<br />
<button onClick={handleSave} disabled={!hasUnsavedChanges}>
Save
</button>
{hasUnsavedChanges && (
<p style={{color: 'orange'}}>⚠️ You have unsaved changes!</p>
)}
{showModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
background: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
}}>
<p>You have unsaved changes. Are you sure you want to leave?</p>
<button onClick={handleConfirmLeave}>Leave</button>
<button onClick={handleCancelLeave} style={{marginLeft: '10px'}}>Stay</button>
</div>
</div>
)}
</div>
);
}
export default MyForm;Advanced Usage
Global Navigation Control
For cases where you need to control navigation from outside the component:
import { guardedNavigateGlobal } from '@mehmetbarisyaman/react-navigation-guard';
import { useNavigate } from 'react-router-dom';
function SomeComponent() {
const navigate = useNavigate();
const handleNavigation = () => {
// This will respect the navigation guard
guardedNavigateGlobal(navigate, '/other-page');
};
return <button onClick={handleNavigation}>Go to Other Page</button>;
}Custom Modal Implementation
import { useNavigationGuard } from '@mehmetbarisyaman/react-navigation-guard';
function FormWithCustomModal() {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [navigationState, setNavigationState] = useState({
show: false,
targetUrl: '',
isReload: false
});
const { allowPendingNavigation, cancelPendingNavigation } = useNavigationGuard({
shouldBlock: () => hasUnsavedChanges,
onNavigationAttempt: (targetUrl, isReload) => {
setNavigationState({
show: true,
targetUrl,
isReload
});
}
});
const handleLeave = () => {
setNavigationState(prev => ({ ...prev, show: false }));
allowPendingNavigation();
};
const handleStay = () => {
setNavigationState(prev => ({ ...prev, show: false }));
cancelPendingNavigation();
};
return (
<div>
{/* Your form here */}
{navigationState.show && (
<CustomModal
title={navigationState.isReload ? "Reload Page?" : "Leave Page?"}
message={`You have unsaved changes. ${
navigationState.isReload
? "Are you sure you want to reload?"
: `Are you sure you want to navigate to ${navigationState.targetUrl}?`
}`}
onConfirm={handleLeave}
onCancel={handleStay}
/>
)}
</div>
);
}API Reference
useNavigationGuard(options)
The main hook for setting up navigation protection.
Parameters
options: NavigationGuardOptions
Options
interface NavigationGuardOptions {
shouldBlock: () => boolean;
onNavigationAttempt: (targetUrl: string, isReload: boolean) => void;
}shouldBlock: A function that returnstrueif navigation should be blockedonNavigationAttempt: Callback fired when navigation is attemptedtargetUrl: The URL being navigated to (empty string for reloads)isReload:trueif this is a page reload attempt,falsefor route changes
Returns
interface NavigationGuardReturn {
allowPendingNavigation: () => void;
cancelPendingNavigation: () => void;
}allowPendingNavigation: Call this to proceed with the blocked navigationcancelPendingNavigation: Call this to cancel the blocked navigation
guardedNavigateGlobal(navigateFn, path)
A utility function for programmatic navigation that respects the active navigation guard.
Parameters
navigateFn: (path: string) => void- The navigate function from useNavigate()path: string- The target path to navigate to
setGlobalNavigationGuard(guardFn)
Lower-level function to set the global guard function. Usually handled automatically by useNavigationGuard.
Parameters
guardFn: ((path: string) => boolean) | null- The guard function or null to remove
How It Works
The navigation guard works by:
- Route Changes: Intercepting programmatic navigation through a global guard function
- Browser Events: Listening to
beforeunloadevents for external navigation - Keyboard Shortcuts: Capturing F5 and Ctrl+R to handle refresh attempts
- State Management: Managing pending navigation state and providing control functions
TypeScript Support
This package is written in TypeScript and includes complete type definitions. All interfaces are exported for use in your applications:
import type {
NavigationGuardOptions,
NavigationGuardReturn
} from '@mehmetbarisyaman/react-navigation-guard';Compatibility
- ✅ React 16.8+ - Full support including latest React 19
- ✅ React Router 6+ - Optimized for modern React Router
- ✅ TypeScript 5+ - Complete type safety
- ✅ Node.js 14+ - Modern build environment
Best Practices
- Keep
shouldBlockpure: Make sure this function only depends on state that determines unsaved changes - Handle all navigation types: Remember to handle both route changes and page reloads in your modal
- Provide clear messaging: Let users know what they'll lose if they navigate away
- Test thoroughly: Test with different navigation methods (back button, direct URL entry, external links)
Troubleshooting
Error: "useNavigate() may be used only in the context of a component"
This error occurs when your component using useNavigationGuard is not wrapped in a Router component.
Solution: Wrap your app in a Router:
// ❌ Wrong - No Router wrapper
import MyForm from './MyForm';
function App() {
return <MyForm />; // This will cause the error
}
// ✅ Correct - Wrapped in Router
import { BrowserRouter } from 'react-router-dom';
import MyForm from './MyForm';
function App() {
return (
<BrowserRouter>
<MyForm />
</BrowserRouter>
);
}Router Options
Choose the appropriate router for your app:
<BrowserRouter>- For web apps with server-side routing support<HashRouter>- For static file hosting or when you can't configure server<MemoryRouter>- For testing or non-browser environments
// For most web applications
import { BrowserRouter } from 'react-router-dom';
// For static hosting (GitHub Pages, etc.)
import { HashRouter } from 'react-router-dom';
// For testing
import { MemoryRouter } from 'react-router-dom';Author
Created by Mehmet Baris Yaman
Connect with me:
- 💼 LinkedIn: mehmetbarisyaman
- 🐦 X (Twitter): @mbarisyaman
- 🐙 GitHub: mehmetbarisyaman
If this package helped you, feel free to follow me for more React utilities and web development content!
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
