0

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.

mellis481
  • 4,332
  • 12
  • 71
  • 118

1 Answers1

2

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' });
    });
  });
}
Besworks
  • 4,123
  • 1
  • 18
  • 34
  • I appreciate the answer. I won't be able to check it before the bounty expires so I'll just give it to you now. May ping you when I have time to test it. – mellis481 May 24 '22 at 21:40
  • Thanks! That's very courteous of you. I'm on here every day so just let me know how it goes when you get the chance. – Besworks May 24 '22 at 21:49