GithubHelp home page GithubHelp logo

Comments (13)

zhantx avatar zhantx commented on July 17, 2024 5

Hi @rickhanlonii , flushSync isn't very suitable in my case. I'm using mobx as the external store, it essentially calls forceUpdate when the store changes.
Say I have 2 class components observing 1 store, if I use flushSync(component.forceUpdate) on both components, it will cause react to perform 2 complete render cycles.
However, if I use runWithPriority(1, component.forceUpdate) on them, react will batch the 2 updates into 1 render cycle

from react.

haskellcamargo avatar haskellcamargo commented on July 17, 2024 1

Same problem here. flushSync is not working for class components when using one of the latest versions of MobX React as it's causing tearing. Functional components were fine after upgrading MobX React but class components are still causing weird UI bugs, making, for instance, different loading placeholders show twice. This only happens with the concurrent renderer though.

from react.

zhantx avatar zhantx commented on July 17, 2024 1

@rickhanlonii mobx is using useSyncExternalStore, however, there is no equivalent API for class component, that's where the tearing happens
consider below code in [email protected] and [email protected]

import React, { useSyncExternalStore } from "react";
import { flushSync, runWithPriority } from "react-dom"; // I patched react-dom to expose runWithPriority

class Store {
  //  implementation of Store...
}
const store = new Store(0);

class DisplayValueClass extends React.Component {
  unsubscribe?: () => void;
  componentDidMount() {
    this.unsubscribe = store.subscribe(() => {
      // Option 1 call forceUpdate directly, this will be in DefaultLane
      // this.forceUpdate();

      // Option 2 wrap forceUpdate in flushSync
      // flushSync(() => this.forceUpdate());

      // Option 3 wrap forceUpdate in flushSync and queueMicrotask
      // queueMicrotask(() => {
      //   flushSync(() => this.forceUpdate());
      // });

      // Option 4 wrap forceUpdate in runWithPriority
      // runWithPriority(1, () => {
      //   this.forceUpdate()
      // });
    });
  }
  componentWillUnmount() {
    this.unsubscribe?.();
  }

  render() {
    return <div>Value: {store.getValue()}</div>;
  }
}

const DisplayValueFunction = () => {
  const storeValue = useSyncExternalStore(store.subscribe, store.getValue);
  return <div>Value: {storeValue}</div>;
};

const App = () => {
  return (
    <>
      <button
        onClick={() => {
          requestAnimationFrame(() => {
            // use requestAnimationFrame to make it outside of event handler, running in DefaultLane
            // this could be a setTimeout as well
            store.setValue(store.getValue() + 1);
          });
        }}
      >
        increment
      </button>
      <DisplayValueClass />
      <DisplayValueClass />
      <DisplayValueFunction />
    </>
  );
};

when the external store updates, because DisplayValueFunction uses useSyncExternalStore, the update is scheduled in SyncLane
For DisplayValueClass, it subscribe to the store in componentDidMount, and the subscriber is to force update itself, we have 4 options here (I attached the main thread snapshots below):

  1. call this.forceUpdate() directly. this will make the update scheduled in DefaultLane
  2. wrap forceUpdate in flushSync, because we have 2 DisplayValueClass instances, they will be in seperate render cycle
  3. wrap forceUpdate in flushSync and queueMicrotask, see screenshot 3, we still have multiple render phase
  4. wrap forceUpdate in unstable_runWithPriority, this is by far the best result

option 1 result
Body

option 2 result
Body (1)

option 3 result
Body (2)

option 4 result
Body (3)

Also, I didn't want to use queueMicrotask because it depends on it is identical to react internal logic for SyncLane, which could change without notifying us. Where if react can expose runWithPriority (which is essentially flushSync without flushPassiveEffects and flushSyncCallbacks) then I can use it with cautious

from react.

rickhanlonii avatar rickhanlonii commented on July 17, 2024

https://react.dev/reference/react-dom/flushSync

from react.

rickhanlonii avatar rickhanlonii commented on July 17, 2024

runWithPriority(1, component.forceUpdate) is essentially the same as:

queueMicrotask(() => {
  flushSync(() => forceUpdate())
})

Can you see if that works?

The tearing is expected if mobx isn't using useSyncExternalStore and if it is, then the extra updates are expected due to the additional renders from the sync updates when the store changes. External stores are not supported in concurrent rendering.

from react.

zhantx avatar zhantx commented on July 17, 2024

also, regarding option 3, queueMicrotas didn't just add 1 extra render, it adds 1 extra render per class component subscriber
image
image

from react.

samcooke98 avatar samcooke98 commented on July 17, 2024

Also... 🙈 - This method is currently exposed (admittedly with a todo) in the experimental channel; https://github.com/facebook/react/blob/main/packages/react-dom/index.experimental.js#L17

from react.

zhantx avatar zhantx commented on July 17, 2024

More consideration on why I prefer runWithPriority over queueMicrotask + flushSync
I could use one queueMicrotask + flushSync to batch several forceUpdate calls, however:

  1. queueMicrotask is a low level API in react internal compare to runWithPriority, I'd prefer a higher level API to expose
  2. flushSync reinstate the behaviour of legacy mode by calling flushPassiveEffects at beginning, where runWithPriority allows me to update class component in concurrent mode with specified Lane. Please correct me if I'm wrong, in my understanding the legacy mode is different from concurrent mode's SyncLane (e.g. in concurrent SyncLane, passiveEffects are flushed at end, and if a passive effect causes UI update, it will be in DefaultLane), which means if I use flushSync on class components, I will have a mixed behaviour of concurrent mode SyncLane and legacy mode

from react.

rickhanlonii avatar rickhanlonii commented on July 17, 2024

This sounds like it's really about batching and not scheduling behavior. Does wrapping it in setTimeout work?

Note: we're removing runWithPriority in React 19.

from react.

zhantx avatar zhantx commented on July 17, 2024

I think eventually what I want to achieve is "schedule class component updates in SyncLane" and "function component and class component get re-rendered together"

If in a click event handler, it calls a setState for a class component, and calls setState (from useState hook) for a function component, then both class component and function component are scheduled in one microtask, and react renders them together

However, when it comes to external store, in a non-event handler, function component still scheduled in SyncLane, but the class component is scheduled in DefaultLane. even if I use queueMicrotask + flushSync, I can't schedule the class component to be in the same microtask as function component

from react.

rickhanlonii avatar rickhanlonii commented on July 17, 2024

Ah right -can you testd in the react canary? In the canary the default updates and sync updates are flushed together so they're batched.

from react.

zhantx avatar zhantx commented on July 17, 2024

I tested react canary, it does batch default updates and sync updates, i.e. for above code example with option 1, that class component and function component are re-rendered together in a microtask, which is what I wanted to achieve. It would be really appreciated if you could link me the PR that introduces this found it, I think it's this one

However, if I remove function component, that only class components are subscribed to the store update, then class components are updated in a task not microtask. this brings more uncertainty to the behaviour

from react.

zhantx avatar zhantx commented on July 17, 2024

@rickhanlonii I just tested react 19 beta, and its behaviour is identical to what I saw in canary that default updates advance to microtask to be flushed with sync updates, however, if there is no sync updates (i.e. no useSyncExternalStore is used), then default updates stays in async task
This is an unreliable fix that doesn't guarantee when class component is updated. I still want to request the runWithPriority API to be reinstated and exposed

from react.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.