Bug: It seems that the concurrent mode does not work as expect

This issue has been tracked since 2022-09-14.

React version: 17.0.1

Steps To Reproduce

Here I use a while loop to simulate a long task.When I click on the div, the animation stops

import React from "react";
import ReactDOM from "react-dom";

const NumberComp = ({ count }) => {
  const start = new Date().getTime();
// Simulate time-consuming tasks
  while (new Date().getTime() - start < 1) {}
  return count;
};
const arr = [];
for (let i = 0; i < 1000; i++) {
  arr.push(i);
}
class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  render() {
    return (
      <>
        <div
          onClick={() => this.setState({ count: this.state.count + 1 })}
          className="animation"
        >{`count:${this.state.count}`}</div>
        {arr.map((i) => (
          <NumberComp key={i} count={i} />
        ))}
      </>
    );
  }
}

ReactDOM.unstable_createRoot(document.getElementById("root")).render(<Home />);

the css

.animation {
    display: block;
     width: 100px;
     height: 100px;
     background: lightyellow;
     animation: myfirst 5s;
     animation-iteration-count: infinite;
}
@keyframes myfirst {
   from {
        width: 30px;
        height: 30px;
        border-radius: 0;
  }
      to {
        width: 200px;
        height: 200px;
        border-radius: 50%;
      }
}

The current behavior

When I click on the div, the animation stops.And the performance is as follows:

image

It seems that the concurrent mode does not work

The expected behavior

Since it's concurrent mode, when I click on the div, the animation shouldn't stop and the page shouldn't freeze

gaearon wrote this answer on 2022-09-15

Not sure about how this works in experimental 17 builds, but the new behavior in 18 is opt-in.

I slightly changed your code to sum up the two counters so that it's clearer that the prop gets passed down.

Here's a version that wraps the update in startTransition. So the counter doesn't update until everything is done:

https://codesandbox.io/s/inspiring-danilo-u9t0kr?file=/src/App.js

import { useState, startTransition } from "react";

const NumberComp = ({ count, parentCount }) => {
  const start = performance.now();
  // Simulate time-consuming tasks
  while (performance.now() - start < 1) {}
  return count + parentCount + " ";
};
const arr = [];
for (let i = 0; i < 1000; i++) {
  arr.push(i);
}

const Home = () => {
  const [count, setCount] = useState(0);

  function handleClick() {
    startTransition(() => {
      setCount((c) => c + 1);
    });
  }

  return (
    <>
      <div onClick={handleClick} className="animation">{`count:${count}`}</div>
      {arr.map((i) => (
        <NumberComp key={i} count={i} parentCount={count} />
      ))}
    </>
  );
};

Here's a version with visual indication that something is happening via changing opacity and useTransition:

https://codesandbox.io/s/damp-leftpad-6m6s6p?file=/src/App.js

import { memo, useState, useTransition } from "react";

const NumberComp = ({ count, parentCount }) => {
  const start = performance.now();
  // Simulate time-consuming tasks
  while (performance.now() - start < 1) {}
  return count + parentCount + " ";
};

const arr = [];
for (let i = 0; i < 1000; i++) {
  arr.push(i);
}

const ExpensiveTree = memo(({ parentCount }) => {
  return arr.map((i) => (
    <NumberComp key={i} count={i} parentCount={parentCount} />
  ));
});

const Home = () => {
  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();

  function handleClick() {
    startTransition(() => {
      setCount((c) => c + 1);
    });
  }

  return (
    <>
      <div
        onClick={handleClick}
        className="animation"
        style={{
          opacity: isPending ? 0.5 : 1
        }}
      >{`count:${count}`}</div>
      <ExpensiveTree parentCount={count} />
    </>
  );
};

Here's a version with useDeferredValue that lets the parent update immediately but the children "lag behind":

https://codesandbox.io/s/brave-thompson-du3y1d?file=/src/App.js

const NumberComp = ({ count, parentCount }) => {
  const start = performance.now();
  // Simulate time-consuming tasks
  while (performance.now() - start < 1) {}
  return count + parentCount + " ";
};

const arr = [];
for (let i = 0; i < 1000; i++) {
  arr.push(i);
}

const ExpensiveTree = memo(({ parentCount }) => {
  return arr.map((i) => (
    <NumberComp key={i} count={i} parentCount={parentCount} />
  ));
});

const Home = () => {
  const [count, setCount] = useState(0);
  const deferredCount = useDeferredValue(count);

  function handleClick() {
    setCount((c) => c + 1);
  }

  return (
    <>
      <div
        onClick={handleClick}
        className="animation"
        style={{
          opacity: count === deferredCount ? 1 : 0.5
        }}
      >
        <b>{`count:${count}`}</b>
      </div>
      <ExpensiveTree parentCount={deferredCount} />
    </>
  );
};

Hope this helps!

lizuncong wrote this answer on 2022-09-15

My demo can also be reproduced in react 18. So I can't call setState directly in react 18, but should I wrap it with useTransition

lizuncong wrote this answer on 2022-09-15

Oh I see, this is not a bug. When I click on the div, the scheduler will schedule a concurrent rendering task with a priority of UserBlockingPriority, and the corresponding expiration time is 250ms. In my demo, since 1000 Number components were rendered, each component took 1ms, and in the previous 250 components it took 250ms. So starting from the 251st component, since the current rendering task has expired, react schedules a synchronous rendering task with the priority ImmediatePriority, so a long task of 750 milliseconds is created, causing the page to freeze

gaearon wrote this answer on 2022-09-15

My demo can also be reproduced in react 18. So I can't call setState directly in react 18, but should I wrap it with useTransition

Updating state during events like clicks is synchronous because the user expects feedback immediately. So yes, deferred updates and concurrency is opt-in.

More Details About Repo
Owner Name facebook
Repo Name react
Full Name facebook/react
Language JavaScript
Created Date 2013-05-24
Updated Date 2022-10-03
Star Count 195560
Watcher Count 6648
Fork Count 40508
Issue Count 1111

YOU MAY BE INTERESTED

Issue Title Created Date Updated Date