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:
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.
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:
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' });
});
});
}