I am using React. I want to warn the user when the user clicks on the back button.
What I had done was
handleWindowClose = (ev) => {
ev.preventDefault();
return ev.returnValue = 'Leaving this page will loose data';
}
componentDidMount = () => {
window.addEventListener('beforeunload',this.handleWindowClose);
}
componentWillUnmount = () => {
window.removeEventListener('beforeunload',this.handleWindowClose);
}
However, this does not work with a back button click. So I tried doing this:
handleWindowClose = (ev) => {
ev.preventDefault();
return ev.returnValue = 'Leaving this page will loose data';
}
onBackButtonEvent = (e) => {
e.preventDefault();
if (confirm("Do you want to loose this data")) {
window.history.go(0);
}
else {
window.history.forward();
}
}
componentDidMount = () => {
window.addEventListener('beforeunload',this.handleWindowClose);
window.addEventListener('popstate',this.onBackButtonEvent);
}
componentWillUnmount = () => {
window.removeEventListener('beforeunload',this.handleWindowClose);
window.removeEventListener('popstate',this.onBackButtonEvent);
}
I am not using react-router. Is there a better way to do this using only React? Also I want the window to stay on that page without using history.forward() as I will lose the window state.
I am having the below problems when handling the back button in React:
The first popstate event is not being called at the first time
It is called twice after executing my back button custom logic
To solve problem 1, I did the following code:
componentDidMount() {
window.history.pushState(null, null, window.location.pathname);
window.addEventListener('popstate', this.onBackButtonEvent);
}
To solve problem 2, I did the below code:
onBackButtonEvent = (e) => {
e.preventDefault();
if (!this.isBackButtonClicked) {
if (window.confirm("Do you want to save your changes")) {
this.isBackButtonClicked = true;
// Your custom logic to page transition, like react-router-dom history.push()
}
else {
window.history.pushState(null, null, window.location.pathname);
this.isBackButtonClicked = false;
}
}
}
Don't forget to add this.isBackButtonClicked = false; in the constructor and unscubscribe the events.
componentWillUnmount = () => {
window.removeEventListener('popstate', this.onBackButtonEvent);
}
I had a similar issue and fixed that this way:
componentDidUpdate(){
window.onpopstate = (e) => {
// Tour code...
}
}
Try this code:
componentDidMount() {
window.addEventListener("popstate", this.onBackButtonEvent)
}
componentWillUnmount() {
window.removeEventListener("popstate", this.onBackButtonEvent)
}
onBackButtonEvent = () => {
window.history.forward()
}
It looks like onbeforeunload is what you want: check this related question, which contains a useful demo.
Also the MDN documentation contain a useful example.
Assuming you've got some good reason for not wanting to use react-router, I'll sketch the JavaScript way of doing this
It looks like you're capturing the wrong event. You want to grab any change of the URL hash, so you should use onhashchange.
Example from the documentation:
if ("onhashchange" in window) {
alert("The browser supports the hashchange event!");
}
function locationHashChanged() {
if (location.hash === "#somecoolfeature") {
somecoolfeature();
}
}
window.onhashchange = locationHashChanged;
However, I'd give react-router a go, given that you're developing in React. In which case, browserHistory would be your friend.
Related
I'm developing with h5p standalone plugin in react (nextjs), passing the path as prop to a Modal Component which render the h5p activity.
useEffect(() => {
const initH5p = async (contentLocation) => {
const { H5P: H5PStandalone } = require('h5p-standalone')
const h5pPath = `https://cdn.thinkeyschool.com/h5p/${contentLocation}`
const options = {
id: 'THINKeyLesson',
h5pJsonPath: h5pPath,
frameJs: '/h5p/dist/frame.bundle.js',
frameCss: '/h5p/dist/styles/h5p.css',
}
let element = document.getElementById('h5p_container')
removeAllChildNodes(element)
await new H5PStandalone(element, options)
fireCompleteH5PTopic(H5P)
setIsLoaderVisible(false)
}
initH5p(location)
}, [location, session.data.user.id, course.slug, topic])
With that code, I get two h5p rendered in screen. So I'm using removeAllChildren() to eliminate them from the render.
function removeAllChildNodes(parent) {
console.log(parent)
while (parent.firstChild) {
parent.removeChild(parent.firstChild)
}
}
That hack is working fine, but when I try to send the xAPI statement to my database, it fires twice
const fireCompleteH5PTopic = async (H5P) => {
H5P.externalDispatcher.on("xAPI", (event) => {
// console.log('event fired')
if (event?.data?.statement?.result?.completion) {
setCounter(counter + 1)
completeH5PTopic(event, session.data.user.id, course.slug, topic)
return true
}
})
}
Any help regarding why it fires twice? I think it may be related to h5p rendering twice too.
Thanks in advance.
I tried using a state to render only once, but it is not working.
I've found this comment to match what I'm looking for. It seems to work, for me, perfectly with "console.log". I want to know when a user has left the page:
const getChatRoomId = () => {}
useEffect(() => {
const chatRoomId = getChatRoomId()
if (chatRoomId) {
console.log('----HAS ENTERED /messages/* ------', chatRoomId)
}
// I believe this is not important?
const routeChangeStart = url => {}
// This is very important
const beforeunload = e => {
if (chatRoomId) {
console.log(`---- IS LEAVING /messages/${chatRoomId} ------`)
}
}
window.addEventListener('beforeunload', beforeunload);
Router.events.on('routeChangeStart', routeChangeStart);
return () => {
window.removeEventListener('beforeunload', beforeunload);
Router.events.off('routeChangeStart', routeChangeStart);
};
}, [someStateChangeVariable])
The above works great but using a function inside beforeunload does not work. I've made some searches and one states to "return a string":
[..]
// This is very important
const beforeunload = e => {
if (chatRoomId) {
alert(`---- IS LEAVING /messages/${chatRoomId} ------`)
}
return 'GoodBye!'
}
[..]
Blocked alert('---- IS LEAVING /messages/1 ------') during beforeunload.
I'm using NextJs catch all routes /pages/[...messages] where you can have a page id of "new" or "123" where "123" is the message id and "new" to create a new message. How to trigger a function when a user leaves a page?
You can use router.events methods: NextJS docs.
Also this topic might be relevant.
I know lots of developers had similar kinds of issues in the past like this. I went through most of them, but couldn't crack the issue.
I am trying to update the cart Context counter value. Following is the code(store/userCartContext.js file)
import React, { createContext, useState } from "react";
const UserCartContext = createContext({
userCartCTX: [],
userCartAddCTX: () => {},
userCartLength: 0
});
export function UserCartContextProvider(props) {
const [userCartStore, setUserCartStore] = useState([]);
const addCartProduct = (value) => {
setUserCartStore((prevState) => {
return [...prevState, value];
});
};
const userCartCounterUpdate = (id, value) => {
console.log("hello dolly");
// setTimeout(() => {
setUserCartStore((prevState) => {
return prevState.map((item) => {
if (item.id === id) {
return { ...item, productCount: value };
}
return item;
});
});
// }, 50);
};
const context = {
userCartCTX: userCartStore,
userCartAddCTX: addCartProduct,
userCartLength: userCartStore.length,
userCartCounterUpdateCTX: userCartCounterUpdate
};
return (
<UserCartContext.Provider value={context}>
{props.children}
</UserCartContext.Provider>
);
}
export default UserCartContext;
Here I have commented out the setTimeout function. If I use setTimeout, it works perfectly. But I am not sure whether it's the correct way.
In cartItemEach.js file I use the following code to update the context
const counterChangeHandler = (value) => {
let counterVal = value;
userCartBlockCTX.userCartCounterUpdateCTX(props.details.id, counterVal);
};
CodeSandBox Link: https://codesandbox.io/s/react-learnable-one-1z5td
Issue happens when I update the counter inside the CART popup. If you update the counter only once, there won't be any error. But when you change the counter more than once this error pops up inside the console. Even though this error arises, it's not affecting the overall code. The updated counter value gets stored inside the state in Context.
TIL that you cannot call a setState function from within a function passed into another setState function. Within a function passed into a setState function, you should just focus on changing that state. You can use useEffect to cause that state change to trigger another state change.
Here is one way to rewrite the Counter class to avoid the warning you're getting:
const decrementHandler = () => {
setNumber((prevState) => {
if (prevState === 0) {
return 0;
}
return prevState - 1;
});
};
const incrementHandler = () => {
setNumber((prevState) => {
return prevState + 1;
});
};
useEffect(() => {
props.onCounterChange(props.currentCounterVal);
}, [props.currentCounterVal]);
// or [props.onCounterChange, props.currentCounterVal] if onCounterChange can change
It's unclear to me whether the useEffect needs to be inside the Counter class though; you could potentially move the useEffect outside to the parent, given that both the current value and callback are provided by the parent. But that's up to you and exactly what you're trying to accomplish.
I'm working an a react app with a few forms and I am trying to implement an edit form for input items. The function first opens the list item in a pre-populated form.
The editItem function currently looks like this:
editItem(event) {
event.preventDefault();
const target = event.target.parentNode.parentNode;
const { key } = target.dataset;
const { className } = target;
const currState = { ...this.state[className] };
const currItem = currState.list[key];
for (let i in currItem) {
if (i !== "list" && i !== "hidden") {
currState[i] = currItem[i]
}
}
this.setState({ [className]: currState });
this.hideUnhide({target: {name: className}});
}
I have confirmed with console logs that currState is correctly set with the values that I am looking for, and that I am not having an async issue. I am using this same format to set state in other functions in my app and all of the others are working properly. If I directly mutate state in the same place, I get the behavior I'm looking for (form fields populate), but nothing happens when I use setState.
Link to my github repo: here. The function in question is in App.js.
As Brian Thompson points out in his comment, it turns out that the hideUnhide function call directly after my setState uses setState as well and writes over the first setState call with the previous state:
hideUnhide(event) {
const { name } = event.target;
const currState = { ...this.state[name] };
if (currState.hidden === true) {
currState.hidden = false;
}
this.setState({ [name]: currState });
}
The way to prevent that was to use hideUnhide as a callback to the setState in editItem:
this.setState({ [className]: currState }, () =>
this.hideUnhide({ target: { name: className } })
);
and now everything functions as intended.
I cant seem to figure out how to show a custom dialogue instead of using the normal window.confirm that routeWillLeave and history.listenBefore uses. Basically i have built a notification system and check if a form is dirty const { dispatch, history, dirty } = this.props;
if the form is dirty it means the user has unsaved changes. If they change route I would like to show my notification which will have two buttons STAY, IGNORE which can both take an onClick handler.
Ive spent a bit of time googling and havent come across any mention of how i might accomplish this using routeWillLeave. The closest thing i could find was to use history.listenBefore however there docs say that I need to do this.
let history = createHistory({
getUserConfirmation(message, callback) {
callback(window.confirm(message)) // The default behavior
}
})
But I am using browserHistory from react-router to initiate my store const history = syncHistoryWithStore(browserHistory, store);
How can I stop a route change after a link has been clicked, show a notification using my custom notification system and depending on which button is clicked either transition to the new route or stay?
Here is an example of how my notification system works and the direction ive headed in which obviously doesn't work because all this returns is a string to show in the window.confirm dialogue by default.
history.listenBefore((location) => {
if(this.props.dirty){
const acceptBtn = {
title: 'STAY',
handler: ignoreRouteChange() //This can be any function I want
}
const denyBtn = {
title: 'IGNORE',
handler: continueRouteChange() //This can be any function I want
}
return dispatch(addNotification({
message: 'You have unsaved changes!',
type: NOTIFICATION_TYPE_WARNING,
duration: 0,
canDismiss: false,
acceptBtn,
denyBtn
}));
return "Usaved changes!"
}
});
Thanks
Another solution that i have decided to use is to return false in the normal setRouterLeaveHook callback and then show my dialog and use the nextLocation passed to the callback to push the next route depending on button selection.
router.setRouteLeaveHook(route, this.routerWillLeave.bind(this));
routerWillLeave(nextLocation){
let { dirty, dispatch, resetForm } = this.props;
if (dirty) {
let dialog = {
id: Date.now(),
showTitle: true,
titleContent: 'Unsaved Changes',
titleIcon: 'fa fa-warning',
content: <span>You have <strong>unsaved</strong> changes! <strong>Discard</strong> them?</span>,
type: 'confirm',
handleCloseClick: (e) => {
e.preventDefault();
dispatch(closeDialog());
},
acceptBtn: {
title: 'Okay',
handler: (e) => {
e.preventDefault();
resetForm();
// Wait for call stack to unwind and then execute
// the following which will now have the updated values.
setTimeout(() => {
dispatch(push(nextLocation));
dispatch(closeDialog());
}, 0);
}
},
denyBtn: {
title: 'Deny',
handler: (e) => {
e.preventDefault();
dispatch(closeDialog());
}
}
}
dispatch(addDialogWindow(dialog));
dispatch(openDialog(false, (e) => dispatch(closeDialog()), false));
return false;
}
return true;
}