简体   繁体   中英

Change document.body, modify React state and change body again

I want to change CSS class of body when user clicks a button, and reverse that when operations defined in handleClick are finished.

I have following handleClick function:

handleClick = data => {
    document.body.classList.add('waiting');
    this.setState({data}, () => document.body.classList.remove('waiting'));
}

I would expect that to work in following way:

  1. Add waiting class to body
  2. Change state of component
  3. Remove waiting class from body

But it doesn't work that way. In order for that to work I have to change handleClick function to something like this:

handleClick = data => {
    document.body.classList.add('waiting');
    setTimeout(() => this.setState({data}, () => document.body.classList.remove('waiting')))
}

I would like to know why this works. I know that setTimeout is 'delaying' execution of this.setState but as far as I know this.setState itself is asynchronous so shouldn't it be delayed on its own? Does that mean that changing document.body.classList is also asynchronous and its execution is delayed more than execution of this.setState ?

But it doesn't work that way.

It does, but that doesn't mean you see anything change in the browser window. :-) What you're doing is exactly what you described: Setting a class before the state change, doing the state change, and removing it after the state change.

 const {useState} = React; class Example extends React.Component { state = { counter: 0, }; handleClick = () => { console.log("Adding class"); document.body.classList.add('waiting'); // NOTE: Normally this should use the callback form of a state setter, // rather than assuming that `this.state.counter` is up-to-date. But // I wanted to keep using the same type of call as you were console.log("Setting state"); this.setState({counter: this.state.counter + 1}, () => { console.log("Removing class"); document.body.classList.remove('waiting'); }); }; componentDidUpdate() { console.log("componentDidUpdate"); } render() { console.log(`render, counter = ${this.state.counter}`); return <div> <div>{this.state.counter}</div> <input type="button" value="Click Me" onClick={this.handleClick} /> </div>; } }; ReactDOM.render(<Example />, document.getElementById("root"));
 <div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

But the after-state-change callback may occur before the DOM is updated, and separately may occur before the page display is updated to account for the change in the body classes.

...but as far as I know this.setState itself is asynchronous so shouldn't it be delayed on its own...

The state update is asynchronous, yes, but may well be done very proactively (it could even be a microtask that runs immediately after the current task completes, which is before the browser updates the page display for the class change — or not, it's an implementation detail of React).

Does that mean that changing document.body.classList is also asynchronous and its execution is delayed more than execution of this.setState ?

No, the change is synchronous, but you don't see the change until the next time the browser paints the display, which won't be until the current task and all of its queued microtasks is complete (or even after that; browsers sync to the refresh rate of your display device, so typically 60 times/second [once every roughly 16.67ms] though it can be more with higher-end monitors and display adapters).

Your setTimeout(/*...*/, 0) trick is one common way to handle this, although the browser may not have updated the display yet when that timer fires (because again, typically it's about 16.67ms between updates). You can hook into the browser's page display update cycle by using requestAnimationFrame , which gives you a callback just before the browser is going to update the display. But it's probably overkill vs. (say) a delay of 20ms.

An alternative way to handle this would be to add the class immediately and use componentDidUpdate to schedule removing it on the next animation frame. But again, that may be overcomplicated.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM