I've am creating a react app and I wanna stop navigation to another page if my form is left dirty
import { useEffect, useState } from "react";
const unsavedChanges = () => {
const [isDirty, setDirty] = useState(false);
useEffect(() => {
function confirmNavigation(event) {
if (isDirty) {
event.preventDefault();
event.returnValue = "Are you sure you want to leave? Your changes will be lost."; // eslint-disable-line no-param-reassign
}
}
window.addEventListener("beforeunload", confirmNavigation);
return () => {
window.removeEventListener("beforeunload", confirmNavigation);
};
}, [isDirty]);
const onDirty = () => setDirty(true);
const onPristine = () => setDirty(false);
return { onDirty, onPristine };
};
export default unsavedChanges;
The code I wrote lets me not reload the page if my form is dirty but I can't prevent it from navigating to another page since react doesn't load the whole page it just loads the data to be changed. I can't use Prompt, useHistory, or useBlocking because they don't exist in react-router v6.4.4.
How can I achieve that?
Why not register a window.onbeforeunload listener? See https://stackoverflow.com/a/1119324/9824103
const unsavedChanges = () => {
const [isDirty, setDirty] = useState(false);
useEffect(() => {
function confirmNavigation(event) {
return isDirty ? "Are you sure you want to leave? Your changes will be lost." : null; // eslint-disable-line no-param-reassign
}
window.onbeforeunload = confirmNavigation;
return () => {
window.beforeunload = null;
};
}, [isDirty]);
const onDirty = () => setDirty(true);
const onPristine = () => setDirty(false);
return { onDirty, onPristine };
};
Related
I am trying to block tab refresh/closing when a user is editing the page. This is stored in state. I have a useEffect that is triggered whenever the isEditing state changes:
const [isEditing, setIsEditing] = useState<boolean>(false);
const handleBrowserCloseReload = (e: any) => {
e.preventDefault();
return (e.returnValue = '');
};
useEffect(() => {
if (isEditing) {
window.addEventListener('beforeunload', handleBrowserCloseReload);
} else {
console.log('remove');
window.removeEventListener('beforeunload', handleBrowserCloseReload);
}
}, [isEditing]);
The problem is even when the 'remove' is logged to the console, I still get the prompt to save changes. Another question is does anyone know the type of the event for this? I do not want to leave it as "any"
Don't bother with an else case, just return a "cleanup" function that removes the handler. When the component re-renders, the cleanup function will run. If you only attach when isEditing is true, then when it is false it won't get added. Plus you have the benefit that if that component unmounts but the page isn't unloaded, the cleanup will also run.
Just make sure to define your handleBrowserCloseReload handler within the useEffect hook so you can reuse the reference.
const [isEditing, setIsEditing] = useState<boolean>(false);
useEffect(() => {
const handleBrowserCloseReload = (e: any) => {
e.preventDefault();
return (e.returnValue = '');
};
if (isEditing) {
window.addEventListener('beforeunload', handleBrowserCloseReload);
}
return () => {
console.log('remove');
window.removeEventListener('beforeunload', handleBrowserCloseReload);
};
}, [isEditing]);
Anyone finding this similar issue, it was fixed with memoizing the handleBrowserCloseReload function:
const handleBrowserCloseReload = useMemo(() => {
return (e: any) => {
e.preventDefault();
return (e.returnValue = '');
};
}, []);
I'm using matchMedia in React to collapse my SideBar when the page is resizing. But the problem is if I refresh the page, my sidebar is open not closed. So if I want to collapse my SideBar I need to resize the page again or use the close button.
const layout = document.getElementById('home-layout');
const query = window.matchMedia('(max-width: 765px)');
query.onchange = (evt) => {
if( query.matches ) {
changeMenuMinified(true);
layout.classList.add('extended-layout');
}
else {
changeMenuMinified(false);
layout.classList.remove('extended-layout');
}
};
query.onchange();
};
useEffect(() => {
window.addEventListener('resize', handleResize);
});
If I remove addEventListener it works, I can reload the page and my sidebar stays closed but if I try to open the sidebar with a button, the sidebar closes quickly
const handleResize = () => {
const layout = document.getElementById('home-layout');
const query = window.matchMedia('(max-width: 765px)');
query.onchange = (evt) => {
if( query.matches ) {
changeMenuMinified(true);
layout.classList.add('extended-layout');
}
else {
changeMenuMinified(false);
layout.classList.remove('extended-layout');
}
};
query.onchange();
};
useEffect(() => {
handleResize()
});
sideBar
Some stuff to consider here:
Initialize your state with the current matching value
Remove listener on effect cleanup function
Don't forget the useEffect dependency array to avoid your code being executed on each render.
You can find a working example here -> https://codesandbox.io/s/stack-72619755-lpwh6m?file=/src/index.js:0-613
const query = window.matchMedia('(max-width: 765px)')
const App = () => {
const [minified, changeMenuMinified] = useState(query.matches)
useEffect(() => {
const resizeHandler = () => {
if (query.matches) {
changeMenuMinified(true)
} else {
changeMenuMinified(false)
}
}
query.addEventListener("change", resizeHandler);
return () => query.removeEventListener("change", resizeHandler);
})
return <p>{minified ? 'minified' : 'expanded'}</p>
}
That's because you need to have both in order to work, on load and also on reside, for that you can just do so:
Notice I added that empty dependencies array.
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
},[]);
I have code block like this
const onRouteChangeStart = React.useCallback(() => {
if (formState.isDirty) {
if (window.confirm('Confirmation message')) {
return true;
}
NProgress.done();
throw "Abort route change by user's confirmation.";
}
}, [formState.isDirty]);
React.useEffect(() => {
Router.events.on('routeChangeStart', onRouteChangeStart);
return () => {
Router.events.off('routeChangeStart', onRouteChangeStart);
};
}, [onRouteChangeStart]);
It works as I want but I want to add a Custom Confirmation Modal instead of Native Confirmation.
When I added, route changes did not stop. That's why I couldn't wait for the user response.
What can I do? Thank you for your responses.
There is a good sample here where it aborts the current route change and saves it to state, prompts the custom model. If confirmed, it pushes the route again.
https://github.com/vercel/next.js/discussions/32231?sort=new?sort=new#discussioncomment-2033546
import { useRouter } from 'next/router';
import React from 'react';
import Dialog from './Dialog';
export interface UnsavedChangesDialogProps {
shouldConfirmLeave: boolean;
}
export const UnsavedChangesDialog = ({
shouldConfirmLeave,
}: UnsavedChangesDialogProps): React.ReactElement<UnsavedChangesDialogProps> => {
const [shouldShowLeaveConfirmDialog, setShouldShowLeaveConfirmDialog] = React.useState(false);
const [nextRouterPath, setNextRouterPath] = React.useState<string>();
const Router = useRouter();
const onRouteChangeStart = React.useCallback(
(nextPath: string) => {
if (!shouldConfirmLeave) {
return;
}
setShouldShowLeaveConfirmDialog(true);
setNextRouterPath(nextPath);
throw 'cancelRouteChange';
},
[shouldConfirmLeave]
);
const onRejectRouteChange = () => {
setNextRouterPath(null);
setShouldShowLeaveConfirmDialog(false);
};
const onConfirmRouteChange = () => {
setShouldShowLeaveConfirmDialog(false);
// simply remove the listener here so that it doesn't get triggered when we push the new route.
// This assumes that the component will be removed anyway as the route changes
removeListener();
Router.push(nextRouterPath);
};
const removeListener = () => {
Router.events.off('routeChangeStart', onRouteChangeStart);
};
React.useEffect(() => {
Router.events.on('routeChangeStart', onRouteChangeStart);
return removeListener;
}, [onRouteChangeStart]);
return (
<Dialog
title="You have unsaved changes"
description="Leaving this page will discard unsaved changes. Are you sure?"
confirmLabel="Discard changes"
cancelLabel="Go back"
isOpen={shouldShowLeaveConfirmDialog}
onConfirm={onConfirmRouteChange}
onReject={onRejectRouteChange}
/>
);
};
I am making a an app where I need to register the keys the user presses. To do that I am using keydown and keyup events. However, I don't want to register the same key multiple times when the user holds it down.
I want to make it so the downHandler function won't run multiple times whenever you hold down a key. I only want it to run once. I try to use an if statement for that and setting keyPressed to true so it won't run again. I try to log keyPressed right after I set it to true, but it logs as false, and the function still runs again.
I tried to remove setKeyPressed(false) in the downHandler function, but keyPressed was still false no matter what.
Here is my app.js:
import { useState, useEffect } from "react";
function App() {
const key = useKeyPress();
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);
function downHandler(key) {
if (keyPressed === false) {
setKeyPressed(true);
console.log(keyPressed)
}
}
const upHandler = (key) => {
console.log("up")
setKeyPressed(false);
};
useEffect(() => {
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, []);
return keyPressed;
}
return (
<div>
</div>
)
}
export default App;
The custom hook should be created in outer scope, when you writing it in the render function (function component body) you are recreating the hook on every render. Hence you losing state each time.
Surprised that you didn't get any lint warning of breaking the rules of hook - writing a hook inside a function, make sure its configured.
Moreover, you are making comparison with stale state keyPressed === false duo to closures, you can fix it by using a reference.
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);
const keyPressedRef = useRef();
useEffect(() => {
keyPressedRef.current = keyPressed;
console.log("keyPressed", keyPressed);
}, [keyPressed]);
useEffect(() => {
function downHandler(key) {
if (keyPressedRef.current === false) {
setKeyPressed(true);
}
}
const upHandler = (key) => {
setKeyPressed(false);
};
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, []);
return keyPressed;
}
export default function App() {
const key = useKeyPress();
return <div></div>;
}
https://codesandbox.io/s/inspiring-jasper-3lym7p?file=/src/App.js:77-897
I have an 'Accept' button which I would like to be automatically clicked after 5 seconds. I'm using React with Next.js. The button code is:
<button name="accept" className="alertButtonPrimary" onClick={()=>acceptCall()}>Accept</button>
If I can't do this, I would like to understand why, so I can improve my React and Next.js skills.
I'm guessing you want this activated 5 seconds after render, in that case, put a setTimeout inside of the useEffect hook, like so. this will call whatever is in the hook after the render is complete.
Although this isn't technically activating the button click event.
useEffect(() => {
setTimeout(() => {
acceptCall()
}, timeout);
}, [])
in that case you should use a ref like so,
const App = () => {
const ref = useRef(null);
const myfunc = () => {
console.log("I was activated 5 seconds later");
};
useEffect(() => {
setTimeout(() => {
ref.current.click();
}, 5000); //miliseconds
}, []);
return (
<button ref={ref} onClick={myfunc}>
TEST
</button>
);
};
Hopefully, this is what you are looking for.
https://codesandbox.io/s/use-ref-forked-bl7i0?file=/src/index.js
You could create a ref for the <button> and set a timeout inside of an effect hook to call the button click event after 5 seconds.
You could throw in a state hook to limit the prompt.
import React, { useEffect, useRef, useState } from "react";
const App = () => {
const buttonRef = useRef("accept-button");
const [accepted, setAccepted] = useState(false);
const acceptCall = (e) => {
alert("Accepted");
};
const fireEvent = (el, eventName) => {
const event = new Event(eventName, { bubbles: true });
el.dispatchEvent(event);
};
useEffect(() => {
if (!accepted) {
setTimeout(() => {
if (buttonRef.current instanceof Element) {
setAccepted(true);
fireEvent(buttonRef.current, "click");
}
}, 5000);
}
}, [accepted]);
return (
<div className="App">
<button
name="accept"
className="alertButtonPrimary"
ref={buttonRef}
onClick={acceptCall}
>
Accept
</button>
</div>
);
};
export default App;