Gregory Kafanov
Gregory Kafanov

Reputation: 357

How to show loader when there is blocking code in onClick handler?

I have a simple react component. The component has a handler that runs when the button is clicked. There is some blocking code in the handler. My task is: when I click the button the loader appears. After that, the blocking code do some heavy operations and after that, the loader disappears:

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [wait, setWait] = useState(true);

  const onClickHandler = () => {
    setWait(true);

    // Blocking code

    setWait(false);
  };

  return (
    <div className="App">
      <button onClick={onClickHandler}>Click</button>
      {wait && <div>Loading...</div>}
    </div>
  );
}

This code is artificial and I know that it is better not to do this - to block the thread. I know that batching works here. But still, I would like to know how I can show the loader in this situation. And in general, is such a situation possible in production, or is it a bad practice?

Upvotes: 0

Views: 634

Answers (3)

MarksASP
MarksASP

Reputation: 506

Blocking the code means that any other operation in your page will have to wait until the first one finished. If you don't care of this happening (maybe this loader covers the entire screen or you don't want the user to do anything else until the operation is done) then that's okay.

Now, maybe you want to perform this opeation while the user interacts with the page, in that case you should make use of Promises' asynchronous behavior, by using the then/catch/finally syntax. You will also need useRef so you can have access to the component's current state in the callbacks.

import { useState, useRef } from "react";
import "./styles.css";

export default function App() {

  // your big operation should return a Promise
  const bigOperation = () => {
    return new Promise((resolve, reject) => {
      resolve("Heeeey");
    })
  }

  // added "_" to the set function so it won't be confused
  // with our function to update the reference
  const [wait, _setWait] = useState(true);

  // create reference for the wait state
  const waitRef = useRef(wait);

  // function to update reference and state (mimics real one _setWait)
  const setWait = (newValue) => {
    waitRef.current = newValue;
    _setWait(newValue);
  }

  const onClickHandler = () => {
    setWait(true);

    // this executes asyncronously
    bigOperation().then(() => {
      setWait(false);
    });
  };

  return (
    <div className="App">
      <button onClick={onClickHandler}>Click</button>
      {wait && <div>Loading...</div>}
    </div>
  );
}

Upvotes: 1

Joseph
Joseph

Reputation: 4725

There are two ways to do this

  1. Execute your blocking code later, let the loading indicator show first.

new Promise or setTimeout will do.

export default function App() {
  const [wait, setWait] = useState(true);

  const onClickHandler = () => {
    setWait(true);

    new Promise((resolve) => {
      // Blocking code
      setWait(false);
      resolve()
    });
  };

  return (
    <div className="App">
      <button onClick={onClickHandler}>Click</button>
      {wait && <div>Loading...</div>}
    </div>
  );
}
  1. Throw your Blocking code to Web Worker.

You can use custom hooks like useWorker to achieve this conveniently

useWorker: https://github.com/alewin/useWorker

Upvotes: 1

Andy
Andy

Reputation: 63550

I'd move the condition outside of the main return statement.

if (wait) return <div>Loading...</div>;

return (
  <div className="App">
    <button onClick={onClickHandler}>Click</button>
  </div>
);

Upvotes: 0

Related Questions