React中含阻塞代码的onClick事件处理器如何显示加载器?
Great question—let's tackle this from both the immediate fix for your loader and the bigger-picture production context.
Fixing the Loader Display
First, let's understand why your current code doesn't show the loader: React uses state batching for updates triggered by event handlers, and your blocking code runs synchronously immediately after setWait(true). This means React doesn't get a chance to render the "Loading..." state before setWait(false) runs, and even if it didn't batch, the blocked main thread would prevent the browser from updating the DOM anyway.
Here are two ways to make the loader appear:
1. Use setTimeout to defer blocking code to the next event loop tick
By wrapping your blocking code in a setTimeout (even with a 0ms delay), you let React finish rendering the loader state before the heavy work starts:
import { useState } from "react"; import "./styles.css"; export default function App() { const [wait, setWait] = useState(false); const onClickHandler = () => { setWait(true); // Push blocking code to after the browser renders the loader setTimeout(() => { // Simulate blocking code (e.g., heavy computation) const start = Date.now(); while (Date.now() - start < 2000) {} setWait(false); }, 0); }; return ( <div className="App"> <button onClick={onClickHandler}>Click</button> {wait && <div>Loading...</div>} </div> ); }
The setTimeout tells the browser: "First finish rendering any pending updates, then run this code."
2. Use flushSync to force immediate state update
If you need the blocking code to run synchronously right after the loader appears (though this still blocks the thread), you can use React's flushSync to skip batching and force the DOM update immediately:
import { useState, flushSync } from "react"; import "./styles.css"; export default function App() { const [wait, setWait] = useState(false); const onClickHandler = () => { // Force React to update the state and render the loader right now flushSync(() => { setWait(true); }); // Blocking code runs after the loader is rendered const start = Date.now(); while (Date.now() - start < 2000) {} setWait(false); }; return ( <div className="App"> <button onClick={onClickHandler}>Click</button> {wait && <div>Loading...</div>} </div> ); }
flushSync bypasses React's automatic batching, ensuring the loader state is applied before the blocking code executes.
Production Context & Best Practices
Does this scenario happen in production?
Yes, but almost never intentionally. You might encounter it if:
- You're running complex synchronous calculations (e.g., parsing large datasets, heavy math operations) directly on the main thread.
- You're using a third-party library that runs synchronous blocking code under the hood.
Is this a bad practice?
Absolutely. Blocking the main thread is one of the worst frontend practices because:
- It freezes the entire UI: users can't click, scroll, or interact with your app until the blocking code finishes.
- It violates React's asynchronous rendering model, which is designed to keep the UI responsive.
The proper production solution
Instead of trying to patch the loader display, you should move the blocking code to a Web Worker. Web Workers run code in a separate background thread, so the main thread stays free to handle UI updates and user interactions. Here's a quick outline:
- Create a separate worker file (e.g.,
calculation.worker.js) that handles the heavy computation. - In your React component, spawn the worker, send it data when the button is clicked, and listen for the result.
- Show the loader when the button is clicked, and hide it when the worker sends back the result.
This way, your loader works correctly and your app stays responsive.
内容的提问来源于stack exchange,提问作者Gregory Kafanov

