I have a hook that executes the function passed to it when someone leaves the current page, or when they leave the website
const useOnPageLeave = (handler) => {
useEffect(() => {
window.onbeforeunload = undefined;
window.addEventListener('beforeunload', (event) => { //When they leave the site
event.preventDefault(); // Cancel the event as stated by the standard.
handler();
});
return () => {
handler(); //When they visit another local link
document.removeEventListener('beforeunload', handler);
};
}, []);
};
When using Firefox or Safari, this alert message appears(only when they leave the site, not when they visit a local link), and I don't want this alert message to show
If I remove the line event.preventDefault(), then the handler function is not executed, and I need the handler event to be executed
Here is an MRE of the problem
const useOnPageLeave = (handler) => {
useEffect(() => {
window.onbeforeunload = () => handler();
window.addEventListener('beforeunload', (event) => {
handler();
});
return () => {
handler();
document.removeEventListener('beforeunload', handler);
};
});
};
There might be a problem with using a listener inside of useEffect.
const useOnPageLeave = (handler) => {
useEffect(() => {
}, []); // useEffect will only get called once on component did mount
};
So You might want to consider trying to take your logic outside of useEffect
or
Adding a useState hook as a listener might be the way to go
const useOnPageLeave = (handler) => {
const [userLeaving, setUserLeaving] = useState(false);
useEffect(() => {
if (userLeaving) handler();
}, [userLeaving]); // useEffect will only get called once on component did mount AND each time userLeaving state updates
};
It looks like firefox and safari browsers are showing this message be default. Not much you can do about it. But there is a good news. It seems they are targeting only beforeunload event but not unload. So you could try to use that.
Here is more info about that: https://support.mozilla.org/en-US/questions/1304313
You can simply handle the unload event on window. For example the following code does an HTTP request while the page is being closed:
window.addEventListener('unload', function (event) {
(async () => {
const response = await fetch('/my-endpoint')
console.log(response)
})()
})
Do note however that the use of the unload event is discouraged. See MDN docs for details (TL;DR: it's unreliable, especially on mobile).
Related
I have a component in my react native app that loads sessions related to a particular individual. In the useEffect() of that component I both load the sessions when the component comes into focus, and unload those sessions within the cleanup.
export const ClientScreen = (props) => {
const isFocused = useIsFocused();
const client = useSelector((state) => selectActiveClient(state));
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
return () => dispatch(unloadSessions()); // Cleaning up here...
};
loadSessions(props);
}
}, [isFocused, client?.id]);
const updatedProps = {
...props,
client,
};
return <ClientBottomTabNavigator {...updatedProps} />;
};
Generally the component is working as expected. However, I do notice that if I load the component with one client, then navigate away, and then come back to the component by loading a new client, that for a brief moment the sessions pertaining to the previous client show before being replaced the sessions relevant to the new client.
My question is, shouldn't the unloadVisits() that runs on cleanup -- which sets sessions to an empty array -- prevent this? Or is this some kind of react behavior that's holding onto the previous state of the component? How can I ensure this behavior doesn't occur?
Cleanup function should appear before the closing-brace of the useEffect hook
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
};
loadSessions(props);
}
return () => dispatch(unloadSessions()); // Cleaning up here... // <--- here
}, [isFocused, client?.id]);
as commented, your loadSessions returns a cleanup function, but you don't do anything with it. And the effect where you call loadSessions(props) does not return anything, that's why it does not clean up.
Edit:
I made a mistake, loadSessions returns a Promise of a cleanup function. And it is impossible to "unwrap" this Promise and get to the cleanup function itself in a way that you can return it in your effect. You have to move the cleaup function out of the async function loadSessions.
But you don't need async/await for everything:
useEffect(() => {
if (isFocused && client?.id) {
loadClientSessions(client.id).then(dispatch);
return () => dispatch(unloadSessions());
}
}, [isFocused, client?.id]);
All examples I am seeing online are using React Components. I am a newbie to react. So any explanation will be helpful, and what I should do to achieve this.
export default function Review() {
...
useEffect(() => {
console.log("useeffect called")
window.addEventListener("beforeunload", save());
}, []);
...
return (...);
Here is a CodeSandbox.io link. Here you will find that I have 2 pages 1 home the other dashboard.
When I go to dashboard. I get the Alert. But Leaving Dashboard I do not get the Alert Message. beforeunload is not working how I expect it to.
https://codesandbox.io/s/react-router-basic-forked-8uvvi?file=/
An alternate way to call a function when a page/component is about to be unmounted is to add it as a return from your useEffect(). For example:
useEffect(() => {
return(() => save())
}, []);
CodeSandbox Example
You are not passing an onbeforeunload callback, you are immediately invoking the alert.
useEffect(() => {
console.log("useeffect called");
// subscribe event
window.addEventListener("beforeunload", alert("HI"));
}, []);
Create/define a callback that does one, or more, of the following:
Prevents the default on the event object
Assigns a string to the event object's returnValue property
Returns a string from the event handler.
window.beforeunload
Note: To combat unwanted pop-ups, some browsers don't display prompts created in beforeunload event handlers unless the page has
been interacted with. Moreover, some don't display them at all.
Code suggestion
useEffect(() => {
const handler = (e) => {
// Cancel the event
e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown
// Chrome requires returnValue to be set
e.returnValue = "";
// ... business logic to save any data, etc... before the window unloads
save();
return "";
};
// subscribe event
window.addEventListener("beforeunload", handler);
return () => {
window.removeEventListener("beforeunload", handler);
};
}, []);
i've got an interesting problem here. I am building a react application using web socket communication with the server. I create this websocket in a useEffect hook, which therefore cannot run multiple times, otherwise i'd end up with multiple connections. In this useEffect, however i intend to use some variables,which are actually in a context (useContext) hook. And when the context values change, the values in useEffect , understandably, don't update. I've tried useRef, but didn't work. Do you have any ideas?
const ws = useRef<WebSocket>();
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
ws.current.addEventListener("message", (message) => {
const messageData: ResponseData = JSON.parse(message.data);
const { response, reload } = messageData;
if (typeof response === "string") {
const event = new CustomEvent<ResponseData>(response, {
detail: messageData,
});
ws.current?.dispatchEvent(event);
} else {
if (reload !== undefined) {
console.log("general info should reload now");
GeneralInfoContext.reload(reload);
}
console.log(messageData);
}
});
});
The web socket is stored as a ref for better use in different functions outside of this useEffect block
Note: the context value to be used is actually a function, GeneralInfoContext.reload()
Solution with split useEffect
You can split the logic that opens the websocket connection vs. the one that adds the message handler into separate useEffects - the first can run once, while the second can re-attach the event every time a dependency changes:
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
}, []);
useEffect(() => {
const socket = ws.current;
if(!socket) throw new Error("Expected to have a websocket instance");
const handler = (message) => {
/*...*/
}
socket.addEventListener("message", handler);
// cleanup
return () => socket.removeEventListener("message", handler);
}, [/* deps here*/])
The effects will run in order so the second effect will run after the first effect has already set ws.current.
Solution with callback ref
Alternatively you could put the handler into a ref and update it as necessary, and reference the ref when calling the event:
const handlerRef = useRef(() => {})
useEffect(() => {
handlerRef.current = (message) => {
/*...*/
}
// No deps here, can update the function on every render
});
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
const handlerFunc = (message) => handlerRef.current(message);
ws.current.addEventListener("message", handlerFunc);
return () => ws.current.removeEventListener("message", handlerFunc);
}, []);
It's important that you don't do addEventListener("message", handlerRef.current) as that will only attach the original version of the function - the extra (message) => handlerRef.current(message) wrapper is necessary so that every message gets passed to the latest version of the handler func.
This approach still requires two useEffect as it's best to not put handlerRef.current = /* func */ directly in the render logic, as rendering shouldn't have side-effects.
Which to use?
I like the first one personally, detaching and reattaching event handlers should be harmless (and basically 'free') and feels less complicated than adding an additional ref.
But the second one avoids the need for an explicit dependency list, which is nice too, especially if you aren't using the eslint rule to ensure exhaustive deps. (Though you definitely should be)
You can provide useEffect with a list of variables and useEffect will re-run when these variables change.
This is a little example:
const [exampleState, setExampleState] = useState<boolean>(false);
useEffect(() => {
console.log("exampleState was updated.");
}, [exampleState]);
An example from reactjs website:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
You should pass an empty array as the second parameter to the useEffect, so it this case it becomes akin to the componentDidMount() logic of react
useEffect(() => {
...your websocket code here
}, [])
My problem is that I need the user to confirm if he wants to continue to refresh the page. If he press No, it won't refresh the page.
Kindly take a look at my development so far:-
useEffect(() => {
window.addEventListener("beforeunload", alertUser);
return () => {
window.removeEventListener("beforeunload", alertUser);
};
}, []);
If you want to display a sort of confirmation before leaving the page then follow the beforeunload event guidelines
According to the specification, to show the confirmation dialog an
event handler should call preventDefault() on the event.
However note that not all browsers support this method, and some
instead require the event handler to implement one of two legacy
methods:
assigning a string to the event's returnValue property
returning a string from the event handler.
To combat unwanted pop-ups, browsers may not display prompts created
in beforeunload event handlers unless the page has been interacted
with, or may even not display them at all.
The HTML specification states that calls to window.alert(),
window.confirm(), and window.prompt() methods may be ignored during
this event. See the HTML specification for more details.
I just tested this in chrome and safari and it works. I don't have a windows box, but this should cover most cases.
useEffect(() => {
const unloadCallback = (event) => {
event.preventDefault();
event.returnValue = "";
return "";
};
window.addEventListener("beforeunload", unloadCallback);
return () => window.removeEventListener("beforeunload", unloadCallback);
}, []);
I think you are looking for this.
https://dev.to/eons/detect-page-refresh-tab-close-and-route-change-with-react-router-v5-3pd
Browser Refresh, Closing Tab, and back and forward buttons, are all explained.
Try this:
useEffect(()=>{
const unloadCallback = (event) => {
const e = event || window.event;
//console.log(e)
e.preventDefault();
if (e) {
e.returnValue = ''
}
return '';
};
window.addEventListener("beforeunload", unloadCallback);
return () => {
//cleanup function
window.removeEventListener("beforeunload", unloadCallback);
}
},[])
How to fix the error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
here' the code:
useEffect(() => {
let ignore = false;
setTimeout(() => {
const fetchData = async () => {
try {
setLoading(true);
setError({});
const response = await getLatest();
if (!ignore) setData(response['data']);
} catch (err) {
setError(err);
}
setLoading(false);
};
fetchData();
});
return () => {
ignore = true;
};
}, []);
The problem here is when I click the home page while loading the data and click the room page. then the error will appeared. How to fix on it?
You need to clean up your timeout on unmount, otherwise, it tries to execute your hooks to update the internal state of the component after it goes out of scope (I assume functions like setLoading and setError are from a React hook).
To do so, put the output of setTimeout in a variable and then call clearTimeout in the function you return from useEffect. That's the clean up function and it's run when the component gets unmounted. So you need something like this:
React.useEffect(() => {
...
const timeout = setTimeout(...)
return () => {
...
clearTimeout(timeout);
}
}, [])
See the docs for more info: https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect