├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── components │ ├── DialogBox.component.tsx │ ├── Header.component.tsx │ └── index.ts ├── hooks │ ├── useBlocker.ts │ └── useCallbackPrompt.ts ├── index.css ├── index.tsx ├── pages │ ├── About.page.tsx │ ├── Home.page.tsx │ └── index.ts ├── react-app-env.d.ts ├── reportWebVitals.ts └── setupTests.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .netlify 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### [Demo Link](https://detect-user-leaving-page-react-router-dom-v6.netlify.app) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-dom-v6-detect-leave-page", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.1", 7 | "@testing-library/react": "^12.1.2", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.4.0", 10 | "@types/node": "^16.11.21", 11 | "@types/react": "^17.0.38", 12 | "@types/react-bootstrap": "^0.32.29", 13 | "@types/react-dom": "^17.0.11", 14 | "bootstrap": "^5.1.3", 15 | "react": "^17.0.2", 16 | "react-bootstrap": "^2.1.1", 17 | "react-dom": "^17.0.2", 18 | "react-router-dom": "^6.2.1", 19 | "react-scripts": "5.0.0", 20 | "typescript": "^4.5.5", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bilal-Bangash/detecting-route-change-react-route-dom-v6/f6e5c74bbdae2bda7e50263dd36b69bd87adb90e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bilal-Bangash/detecting-route-change-react-route-dom-v6/f6e5c74bbdae2bda7e50263dd36b69bd87adb90e/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bilal-Bangash/detecting-route-change-react-route-dom-v6/f6e5c74bbdae2bda7e50263dd36b69bd87adb90e/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | 2 | .App-logo { 3 | height: 40vmin; 4 | pointer-events: none; 5 | } 6 | 7 | @media (prefers-reduced-motion: no-preference) { 8 | .App-logo { 9 | animation: App-logo-spin infinite 20s linear; 10 | } 11 | } 12 | 13 | .App-header { 14 | background-color: #282c34; 15 | min-height: 100vh; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | font-size: calc(10px + 2vmin); 21 | color: white; 22 | } 23 | 24 | .App-link { 25 | color: #61dafb; 26 | } 27 | 28 | @keyframes App-logo-spin { 29 | from { 30 | transform: rotate(0deg); 31 | } 32 | to { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | .me-auto a{ 37 | color: white; 38 | text-decoration: none; 39 | } 40 | .me-auto a:hover{ 41 | color: greenyellow; 42 | text-decoration: none; 43 | } 44 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter, Route, Routes } from 'react-router-dom' 3 | import { About, Home } from './pages' 4 | import './App.css' 5 | import { Header } from './components' 6 | 7 | function App() { 8 | return ( 9 |
10 | 11 |
12 | 13 | } /> 14 | } /> 15 | 16 | 17 |
18 | ) 19 | } 20 | 21 | export default App 22 | -------------------------------------------------------------------------------- /src/components/DialogBox.component.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal } from 'react-bootstrap' 2 | 3 | interface DialogBoxProps { 4 | showDialog: boolean 5 | cancelNavigation: any 6 | confirmNavigation: any 7 | } 8 | 9 | const DialogBox: React.FC = ({ 10 | showDialog, 11 | cancelNavigation, 12 | confirmNavigation, 13 | }) => { 14 | return ( 15 | 16 | 17 | Warning 18 | 19 | 20 | There are some changes? 21 |
Are you sure you want to navigate!!!! 22 |
23 | 24 | 27 | 30 | 31 |
32 | ) 33 | } 34 | export default DialogBox 35 | -------------------------------------------------------------------------------- /src/components/Header.component.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Nav, Navbar } from 'react-bootstrap' 2 | import { NavLink } from 'react-router-dom' 3 | 4 | function Header() { 5 | return ( 6 | 7 | 8 | MB 9 | 13 | 14 | 15 | ) 16 | } 17 | 18 | export default Header 19 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header.component' 2 | export { default as DialogBox } from './DialogBox.component' 3 | -------------------------------------------------------------------------------- /src/hooks/useBlocker.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { UNSAFE_NavigationContext } from 'react-router-dom'; 3 | import type { History, Blocker, Transition } from 'history'; 4 | 5 | export function useBlocker(blocker: Blocker, when = true): void { 6 | const navigator = React.useContext(UNSAFE_NavigationContext) 7 | .navigator as History; 8 | 9 | React.useEffect(() => { 10 | if (!when) return; 11 | 12 | const unblock = navigator.block((tx: Transition) => { 13 | const autoUnblockingTx = { 14 | ...tx, 15 | retry() { 16 | unblock(); 17 | tx.retry(); 18 | }, 19 | }; 20 | 21 | blocker(autoUnblockingTx); 22 | }); 23 | 24 | return unblock; 25 | }, [navigator, blocker, when]); 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/useCallbackPrompt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { useCallback, useEffect, useState } from 'react' 3 | import { useLocation, useNavigate } from 'react-router' 4 | import { useBlocker } from './useBlocker' 5 | 6 | export function useCallbackPrompt(when: boolean): (boolean | (() => void))[] { 7 | const navigate = useNavigate() 8 | const location = useLocation() 9 | const [showPrompt, setShowPrompt] = useState(false) 10 | const [lastLocation, setLastLocation] = useState(null) 11 | const [confirmedNavigation, setConfirmedNavigation] = useState(false) 12 | 13 | const cancelNavigation = useCallback(() => { 14 | setShowPrompt(false) 15 | setLastLocation(null) 16 | }, []) 17 | 18 | // handle blocking when user click on another route prompt will be shown 19 | const handleBlockedNavigation = useCallback( 20 | (nextLocation) => { 21 | // in if condition we are checking next location and current location are equals or not 22 | if ( 23 | !confirmedNavigation && 24 | nextLocation.location.pathname !== location.pathname 25 | ) { 26 | setShowPrompt(true) 27 | setLastLocation(nextLocation) 28 | return false 29 | } 30 | return true 31 | }, 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | [confirmedNavigation, location] 34 | ) 35 | 36 | const confirmNavigation = useCallback(() => { 37 | setShowPrompt(false) 38 | setConfirmedNavigation(true) 39 | }, []) 40 | 41 | useEffect(() => { 42 | if (confirmedNavigation && lastLocation) { 43 | navigate(lastLocation.location?.pathname) 44 | 45 | // Clean-up state on confirmed navigation 46 | setConfirmedNavigation(false) 47 | } 48 | // eslint-disable-next-line react-hooks/exhaustive-deps 49 | }, [confirmedNavigation, lastLocation]) 50 | 51 | useBlocker(handleBlockedNavigation, when) 52 | 53 | return [showPrompt, confirmNavigation, cancelNavigation] 54 | } 55 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import 'bootstrap/dist/css/bootstrap.min.css'; 5 | import App from './App'; 6 | import reportWebVitals from './reportWebVitals'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/pages/About.page.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col } from 'react-bootstrap' 2 | 3 | const About = () => { 4 | return ( 5 | 6 | 7 | About Page 8 | 9 | 10 | ) 11 | } 12 | 13 | export default About 14 | -------------------------------------------------------------------------------- /src/pages/Home.page.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Container, Row, Col, Button, Form } from 'react-bootstrap' 3 | import { DialogBox } from '../components' 4 | import { useCallbackPrompt } from '../hooks/useCallbackPrompt' 5 | 6 | const Home = () => { 7 | const [state, setState] = useState({}) 8 | const [showDialog, setShowDialog] = useState(false) 9 | const [showPrompt, confirmNavigation, cancelNavigation] = 10 | useCallbackPrompt(showDialog) 11 | 12 | const handleChange = (event: any) => { 13 | const { name, value } = event.target 14 | setState({ ...state, [name]: value }) 15 | setShowDialog(true) 16 | } 17 | return ( 18 | 19 | 25 | 26 | 27 | 28 |
29 | 30 | Name 31 | 37 | 38 | 39 | 40 | Designation 41 | 47 | 48 | 51 |
52 | 53 | 54 |
55 |
56 | ) 57 | } 58 | 59 | export default Home 60 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Home } from './Home.page' 2 | export { default as About } from './About.page' 3 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------