React state variables mysteriously changing before 'beforeunload' function - javascript

UPDATE:
I created a minimum reproducible sample here:
https://react-wsaei2.stackblitz.io
Editor link: https://stackblitz.com/edit/react-wsaei2
In my app I'm using a sendBeacon function in beforeunload to unlock a database document when someone closes the page without doing it themselves. Here's how I'm doing that in a useEffect with an empty array so it only runs once on start:
// Add event listener to run code before window closes
window.addEventListener("beforeunload", unlockStory);
return () => {
window.removeEventListener("beforeunload", unlockStory);
}
And here's the unlockStory function:
const unlockStory = (e) => {
e.preventDefault();
if (props.storyID && !success && !loggedOut) {
console.log("Unlocking story. success, loggedOut", success, loggedOut);
debugger;
navigator.sendBeacon(`/api/stories/${props.storyID}/${loggedOut}/${success}`, JSON.stringify({body: {locked: true}}));
}
e.returnValue = "What's going on?";
}
As you see I don't want the beacon to send every time - only if the loggedOut == false (i.e. I don't want to send the Beacon to unlock if the user is already logged out).
The problem is for some reason in the unlockStory function loggedOut is always false, even if it was true right beforehand! I put a manual refresh button on the page to check like so:
const handleRefresh = () => {
console.log("Handling refresh: success, loggedOut", success, loggedOut);
window.location.reload(false);
}
The output is:
Handling refresh: success, loggedOut false true
Unlocking story. success, loggedOut false false
WHYYY????
Another odd thing is that the debugger; line in the unlockStory function doesn't ever get triggered on normal refreshes or page closes, it only gets triggered on refreshes that are causes by me making file changes so npm automatically refreshes the open pages.
Please help, I'm at a loss, thank you!

You need to define success and loggedOut (and other used variables) as effect dependencies.
useEffect(() => {
const unlockStory = (e) => {
e.preventDefault();
console.log("Inside unlockStory. success, loggedOut:", success, loggedOut);
if (!success && !loggedOut) {
console.log("Inside if")
navigator.sendBeacon(`/api/stories/`, JSON.stringify({body: {locked: true}}));
}
e.returnValue = "What's going on?";
}
// Add event listener to run code before window closes
window.addEventListener("beforeunload", unlockStory);
return () => {
window.removeEventListener("beforeunload", unlockStory);
}
}, [success, loggedOut]); // Run on every success or loggedOut change
There is no need to manually remove event listeners as:
React also cleans up effects from the previous render before running the effects next time.
Sometimes is easier to use class based components:
class StartClass extends React.Component {
constructor(props) {
super(props);
this.state = {
loggedOut: false,
success: false,
storyObj: {
segCount: 2
}
}
}
componentDidMount() {
this.setState( { loggedOut: true } );
window.addEventListener("beforeunload", this.unlockStory);
return () => {
window.removeEventListener("beforeunload", this.unlockStory);
}
}
handleRefresh = () => {
let {success, loggedOut} = this.state
console.log("Handling refresh: success, loggedOut", success, loggedOut);
window.location.reload(true);
}
unlockStory = (e) => {
e.preventDefault();
let {success, loggedOut} = this.state
console.log("Inside unlockStory. success, loggedOut:", success, loggedOut);
if (!success && !loggedOut) {
console.log("Inside if")
navigator.sendBeacon(`/api/stories/`, JSON.stringify({body: {locked: true}}));
}
e.returnValue = "What's going on?";
}
render() {
return (
<Container fluid>
<Row>
<Col>
<p>
To reproduce:<br/> Open console, then click the Refresh page button below. When the alert pops up check the console and you'll see that the loggedOut variable has changed from true to false.
</p>
<p>
The same behavior occurs when you refresh via the browser, but you can't see the log as the console is cleared upon refresh.
</p>
{this.state.loggedOut && <Button onClick={this.handleRefresh}>Refresh page</Button>}
</Col>
</Row>
</Container>
);
}
}

Related

How to prevent multiplication of ipcRenderer listenters?

I have a button in my react application that needs to do something 1 time every time it's clicked. Currently, when clicked, the listener seems to multiply.
Here is brief example of the code that I'm using:
// In App.js
const App = () => {
const buttonHandler = () => {
api.myApi.rendererToMain();
api.myApi.mainToRenderer();
};
return (
<div>
<Button cb={buttonHandler} />
</div>
);
};
// In Button.js
const Button = (props) => {
return (
<button type="button" onClick={ (e) => { props.cb() }}>Send Ping</button>
)
};
// In main.js
ipcMain.on('async', (e, msg) => {
console.log(msg); // print ping
e.reply('async_response', 'pong');
});
// In preload.js
contextBridge.exposeInMainWorld('api', {
myApi: {
rendererToMain: () => {
ipcRenderer.send('async', 'ping');
},
mainToRenderer: () => {
ipcRenderer.on('async_response', (e, msg) => {
console.log(msg);
});
},
},
});
When I run the electron app, I have a terminal for the Main process open, and devTools open for output from the renderer. Currently, when the button is pressed 3 times, the result looks like:
// In the browserWindow devTools console
pong
(2)pong
(5)pong
// In the Main process console
ping
ping
ping
The desired output is only different for the renderer console:
pong
pong
pong
My attempted solution
My first attempt at solving this on my own, after some stackoverflow research, was to try and remove the ipcRenderer listener for the "async_response" channel. All attempts to do this resulted in no output in the renderer console, and all 3 expected pings in the Main process console.
For example:
// Change made in preload.js
contextBridge.exposeInMainWorld('api', {
myApi: {
rendererToMain: () => {
ipcRenderer.send('async', 'ping');
},
mainToRenderer: () => {
ipcRenderer.on('async_response', (e, msg) => {
console.log(msg); // With below change, this does nothing.
});
// Attempt to remove the listener now?
// Seems to remove the listener before any output logged.
ipcRenderer.removeAllListeners('async_response');
},
},
});
If anyone could help me understand where and how to stop the listeners from multiplying, I would be eternally grateful. Thank you in advance.
After some sleep, I realized that my search was too narrow. I found that the ipcRenderer.on() method was being subscribed to an additional time each time the button was pressed, as per the concepts in this post: Node .on method firing too many times
Understanding this, I made the following change to my code, such that the call to the ipcRenderer.on() method only occurred once:
// In App.js
const App = () => {
const buttonHandler = () => {
api.myApi.rendererToMain();
// api.myApi.mainToRenderer(); <- Removed this call to the ipcRenderer.on()
};
return (
<div>
<Button cb={buttonHandler} />
</div>
);
};
// In Button.js
const Button = (props) => {
const callbackHandler = () => {
props.cb();
};
// Subscribe to listener on component construction
api.myApi.mainToRenderer();
return (
<button type="button" onClick={ (e) => { callbackHandler() }}>Send Ping</button>
)
};
These changes result in exactly the expected behavior. After three clicks of the button, I see in the renderer console:
(3)pong
Going to leave this unanswered for a time. I'm definitely open to comments about my fix and implementation all around.

How to use ReactJS to give immediate feedback to user before a long operation?

NOTE: Marking Michael Cox's answer below correct, but you should read the discussion underneath the answer as well, which illustrates that componentDidMount is apparently called BEFORE browser render.
I have a ReactJS app, and when the user clicks a button, I'd like to change the button text to "Working..." so that the user can see that the app is working on their request. AFTER the updated button has been rendered, then and only then, do I want to actually start the operation.
My first attempt was to use componentDidUpdate. AFAIK, it shouldn't be called until AFTER rendering. But here is my attempt, and you can see the button never changes (open up console to help it take long enough). https://jsfiddle.net/joepinion/0s78e2oj/6/
class LongOperationButton extends React.Component {
constructor(props) {
super(props);
this.state = {
opInProgress: false,
};
}
render() {
let btnTxt = "Start Operation";
if(this.state.opInProgress) {
btnTxt = "Working...";
}
return(
<button
onClick={()=>this.startOp()}
>
{btnTxt}
</button>
);
}
startOp() {
if(!this.state.opInProgress) {
this.setState({opInProgress: true});
}
}
componentDidUpdate() {
if(this.state.opInProgress) {
this.props.op();
this.setState({opInProgress: false});
}
}
}
function takeALongTime() {
for(let i=1;i<=2000;i++) {
console.log(i);
}
}
ReactDOM.render(
<LongOperationButton op={takeALongTime} />,
document.getElementById('container')
);
Next attempt I tried using the callback of setState. Seems less correct because it bypasses componentShouldUpdate, but worth a shot: https://jsfiddle.net/joepinion/mj0e7gdk/14/
Same result, the button doesn't update.
What am I missing here???
laurent's comment is dead on. You're code is executing this.props.op, then immediately updating the state. TakeALongTime needs to signal when it's done.
function takeALongTime() {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
and componentDidUpdate needs to wait until op is done before setting the state.
componentDidUpdate() {
if(this.state.opInProgress) {
this.props.op().then(() => {
this.setState({opInProgress: false});
});
}
}
(This is going off of your first example.)
You could also do this with callbacks.
function takeALongTime(cb) {
setTimeout(cb, 1000);
}
componentDidUpdate() {
if(this.state.opInProgress) {
var parentThis = this;
this.props.op(function() {
parentThis.setState({opInProgress: false});
});
}
}

Change screen while function runs then change back

I'm trying to change the screen when a submit button is pressed. The page is essentially just a spinner page. I want it to change to the page while the data is submittied to the database, then change back once the database stuff is finished.
I can get the screen to change but can't get it to change back. Well, I did once, but as soon as I started adding code to the functions it stopped working entirely.
I have a main homepage that has a table component
class Home extends Component {
constructor() {
super()
this.state = {
data: [],
loading: false
};
}
handleSpinner() {
this.setState({ loading: !this.state.loading })
}
handleCancel() {
if (confirm('Are you sure you want to clear the table?')) {
this.setState({ data: [] })
} else {
return
}
}
render() {
return (
this.state.loading ? <WaitingSpinner /> :
<div>
<MainTable data={this.state.data} handleCancel={this.handleCancel} handleSpinner={this.handleSpinner} />
</div>
);
}
The table has a submit button that calls a submit handler.
handleSubmit() {
this.props.handleSpinner()
this.writeToDatabase(this.props.data)
this.props.handleSpinner()
}
async writeToDatabase(data) {
await data.map(test => {
console.log('DATA ', test)
axios.post(`database stuff')
.then(function (response) { console.log(response) }).catch(function (error) { console.log('Err ', error) })
})
}
My goal here is to change to the spinner, run the database function, once that completes it changes back.
At this point it changes to the spinner screen but never changes back to the main table page. I'm guessing it has something to do with when the state actually gets updated but I don't know enough about it to track down what is going on.

How can I mimic onbeforeunload in a Vue.js 2 application?

I have a Vue component that is tracking when it is "dirty" (e.g. unsaved). I would like to warn the user before they browse away from the current form if they have unsaved data. In a typical web application you could use onbeforeunload. I've attempted to use it in mounted like this:
mounted: function(){
window.onbeforeunload = function() {
return self.form_dirty ? "If you leave this page you will lose your unsaved changes." : null;
}
}
However this doesn't work when using Vue Router. It will let you navigate down as many router links as you would like. As soon as you try to close the window or navigate to a real link, it will warn you.
Is there a way to replicate onbeforeunload in a Vue application for normal links as well as router links?
Use the beforeRouteLeave in-component guard along with the beforeunload event.
The leave guard is usually used to prevent the user from accidentally
leaving the route with unsaved edits. The navigation can be canceled
by calling next(false).
In your component definition do the following:
beforeRouteLeave (to, from, next) {
// If the form is dirty and the user did not confirm leave,
// prevent losing unsaved changes by canceling navigation
if (this.confirmStayInDirtyForm()){
next(false)
} else {
// Navigate to next view
next()
}
},
created() {
window.addEventListener('beforeunload', this.beforeWindowUnload)
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.beforeWindowUnload)
},
methods: {
confirmLeave() {
return window.confirm('Do you really want to leave? you have unsaved changes!')
},
confirmStayInDirtyForm() {
return this.form_dirty && !this.confirmLeave()
},
beforeWindowUnload(e) {
if (this.confirmStayInDirtyForm()) {
// Cancel the event
e.preventDefault()
// Chrome requires returnValue to be set
e.returnValue = ''
}
},
},
The simplest solution to mimic this fully is as follow:
{
methods: {
beforeWindowUnload (e) {
if (this.form_dirty) {
e.preventDefault()
e.returnValue = ''
}
}
},
beforeRouteLeave (to, from, next) {
if (this.form_dirty) {
next(false)
window.location = to.path // this is the trick
} else {
next()
}
},
created () {
window.addEventListener('beforeunload', this.beforeWindowUnload)
},
beforeDestroy () {
window.removeEventListener('beforeunload', this.beforeWindowUnload)
}
}

How can i use localStorage to maintain state after a page refresh in React

Each time a user logs in, i want the state to remain at 'true' even if there is a page reload.
The state is set initially to false, (let _authed = false).
But when i reload the page, it goes back to false, which is the index page.
What i did
When the user logs in, i save the user's details in localStorage and when the user logs out, i cleared the localStorage and i set it to false. (this works fine)
In the setAuthed() function, i tried to check if the user i stored in localStorage is not null, it should keep the authed variable to true.
But its not working when i refresh the page. Is there anything, i am doing wrong? Help appreciated.
let _authed = false;
// User logs in, set user in localStorage
saveUser(user){
_user = user;
localStorage.setItem('user', JSON.stringify(user))
},
//User clicks logout, set state to false
setLogout(){
_authed = false;
localStorage.clear()
},
// If there is a user in local storage, always set state to true
setAuthed(){
if (localStorage.getItem("user") !== null) {
_authed = true;
}
},
getAuthed(){
return _authed;
},
You can use React lifecycle methods to read and write some kind of persistent state. Here I've wrote a tiny example to show it.
class Root extends React.Component {
state = {
isLoggedIn: false
}
componentDidMount () {
const persistState = localStorage.getItem('rootState');
if (persistState) {
try {
this.setState(JSON.parse(persistState));
} catch (e) {
// is not json
}
}
}
componentWillUnmount () {
localStorage.setItem('rootState', JSON.stringify(this.state);
}
render () {
return (
<div>
{this.props.children}
</div>
)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

Categories

Resources