'Why does setting state in componentDidMount require setTimeout to work?
I have a component that looks something like the below (yes, I know React hooks exist...). I've created a CodeSandbox example too.
export class App extends React.Component {
state = {
loadingStatus: "idle"
};
componentDidMount() {
debugger;
setTimeout(() => {
this.setState({ loadingStatus: "loading" });
}, 1);
setInterval(() => {
const loadingStatus =
this.state.loadingStatus === "loading" ? "complete" : "loading";
this.setState({ loadingStatus });
}, 3000);
}
render() {
const { loadingStatus } = this.state;
return (
<div>
<div>loadingStatus: {this.state.loadingStatus}</div>
<div role="status">
{loadingStatus === "loading" && (
<img
alt="loading"
src="https://upload.wikimedia.org/wikipedia/commons/b/b1/Loading_icon.gif?20151024034921"
/>
)}
{loadingStatus === "complete" && (
<span className="visually-hidden">Loading has completed</span>
)}
</div>
{loadingStatus === "complete" && (
<div>Content has loaded. The page will reload again momentarily.</div>
)}
</div>
);
}
}
This component demonstrates how to make a page loading indicator that is accessible to screen reader users. The key is the use of an element with role="status" which will announce when its content changes. It needs to contain:
- nothing initially so that, when it does have content, the new content will be announced.
- the loading indicator when the component is in a loading state which should occur immediately after the component mounts.
- a visually hidden element after loading is complete which will announce to screen reader users that loading is complete.
The issue I'm having is that when I change the loading status from "idle" to "loading" in componentDidMount, it doesn't "register" in the DOM unless I wrap it in a setTimeout(), even just a 1 ms delay. If I don't have the setTimeout(), the content change in my role="status" element is not announced.
What's interesting is that, if you remove the setTimeout() and open up dev tools in the browser, the debugger; breaks and you can see that the UI is rendered when the status is "idle". This has me confused why there is a need for a delay.
To be clear, the problem is that, without the setTimeout(), the initial announcing of "loading" does not occur. You'll need a screen reader (eg. NVDA) to test.
Thanks in advance.
Solution 1:[1]
The componentDidMount method is called after the component has been rendered. This is why you can see it when you add the debugger statement.
Calling setState inside componentDidMount will then actually trigger a second call to render. Which reads the new state and updates the DOM.
I'm not sure why you were using setInterval but if you simplify your example it actually works the way you would expect.
componentDidMount() {
this.setState({ loadingStatus: 'loading' });
setTimeout(() => {
this.setState({ loadingStatus: 'complete' });
}, 3000);
}
Which causes the following flow:
- state = { loadingStatus: 'idle' }
- render
- componentDidMount
- setState({ loadingStatus: 'loading' })
- render
- 3 seconds later... setState({ loadingStatus: 'complete' })
- render
In the example above setTimeout(..., 3000) represents a simulated API call. You would most likely want to do an asynchronous operation instead like this :
componentDidMount() {
this.setState({ loadingStatus: 'loading' });
fetch('/your/api/endpoint').then(response => {
// do something with the response
this.setState({ loadingStatus: 'complete' });
});
}
If you're still having trouble with it after trying the above method, it might be because setState is async and the changes you make can be batched if they happen in one continuous execution flow. Wrapping your code in a requestAnimationFrame callback will ensure that the previous render is complete before updating the status.
componentDidMount() {
requestAnimationFrame(() => {
this.setState({ loadingStatus: 'loading' });
fetch('/your/api/endpoint').then(response => {
// do something with the response
this.setState({ loadingStatus: 'complete' });
});
});
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
