How to avoid UI freezing with React

Consider this little react app:

import React, { Component } from "react";
import PropTypes from "prop-types";

import { Audio } from "react-loader-spinner";

import "./styles.css";

async function blockingUI() {
  let x = 0;
  for (let i = 0; i < 10000; i++) {
    for (let j = 0; j < 10000; j++) {
      x = Math.sqrt(x + i * j);
    }
  }
  return x;
}

async function setTimeoutAsync(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

class Foo extends Component {
  static propTypes = {
    classes: PropTypes.object,
    style: PropTypes.object,
  };

  static defaultProps = {};

  // -------- Public: Reactjs --------
  constructor(props) {
    super(props);
    this.state = {
      ...props,
      isLoading: false,
    };
  }

  setLoading = (isLoading) => {
    this.setState({
      isLoading: isLoading,
    });
  };

  setLoadingAsync(isLoading) {
    return new Promise((resolve) => {
      this.setState({
        isLoading: isLoading
      }, resolve)
    });
  }

  onBlockingUi = async () => {
    let widget = this;
    widget.setLoading(true);
    console.time("blockingUI");
    await blockingUI();
    console.timeEnd("blockingUI");
    widget.setLoading(false);
  };

  onBlockingUiHacked = async () => {
    let widget = this;
    widget.setLoading(true);
    await setTimeoutAsync(20);
    console.time("blockingUI");
    await blockingUI();
    console.timeEnd("blockingUI");
    widget.setLoading(false);
  };

  onNonBlockingUi = async () => {
    let widget = this;
    widget.setLoading(true);
    await setTimeoutAsync(1000);
    widget.setLoading(false);
  };

  render() {
    const loader = this.state.isLoading ? (
      <Audio heigth="100" width="100" color="grey" ariaLabel="loading" />
    ) : null;
    return (
      <div className="App">
        <h1>Loading test</h1>
        <h2>Click to see some magic happen!</h2>
        <button onClick={this.onBlockingUi}>Blocking UI</button>
        <br/>
        <button onClick={this.onBlockingUiHacked}>Blocking UI + hack</button>
        <br/>
        <button onClick={this.onNonBlockingUi}>Non-blocking UI</button>
        {loader}
      </div>
    );
  }
}

export default function App() {
  return <Foo />;
}

In the above example there are 3 buttons:

  1. When I press the one that says “Blocking UI” the spinner widget won’t show up and the UI will freeze up.
  2. When I press the one that says “Blocking UI + hack” the spinner widget will show up but it won’t update/re-render properly (UI frozen up)
  3. When I press the one that says “Non blocking UI”, everything works ok.

I’d like to know what’d be the best practice here so I can avoid blocking the UI when trying to run these type of CPU intensive functions such as blockingUI while displaying some loading html elements.

You can find the codesanbox here: https://codesandbox.io/s/sharp-roman-bouy9l