Comments (13)
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.
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.
@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):
- call
this.forceUpdate()
directly. this will make the update scheduled in DefaultLane - wrap forceUpdate in flushSync, because we have 2 DisplayValueClass instances, they will be in seperate render cycle
- wrap forceUpdate in flushSync and queueMicrotask, see screenshot 3, we still have multiple render phase
- wrap forceUpdate in unstable_runWithPriority, this is by far the best result
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.
https://react.dev/reference/react-dom/flushSync
from react.
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.
also, regarding option 3, queueMicrotas didn't just add 1 extra render, it adds 1 extra render per class component subscriber
from react.
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.
More consideration on why I prefer runWithPriority
over queueMicrotask
+ flushSync
I could use one queueMicrotask
+ flushSync
to batch several forceUpdate
calls, however:
queueMicrotask
is a low level API in react internal compare torunWithPriority
, I'd prefer a higher level API to exposeflushSync
reinstate the behaviour of legacy mode by callingflushPassiveEffects
at beginning, whererunWithPriority
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.
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.
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.
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.
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.
@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)
- Bug: Input of type number is incrementing/decrementing on mouse scroll
- Bug: render time differ depending on load
- Bug: StrictMode is not preventing side effects HOT 2
- [DevTools Bug]: After updating to version 5.1 the Dev tools fails to inspect elements
- Bug: HOT 1
- [React 19] Async transitions race condition handling
- Bug: React 18.3 warning says to import `act` from `react`, only `unstable_act` exists HOT 3
- Bug: "React Developer Tools" Chrome Extension unable to profile page rendering
- Bug: Source not displayed in DevTools HOT 2
- Bug:
- [React 19] useTransition()'s pending state does not go back to false (revision 94eed63c49-20240425) HOT 3
- Changelog for 18.3 is missing HOT 1
- [React 19] Removal of `ReactDOM.findDOMNode` HOT 2
- [React 19] react-test-renderer deprecation HOT 2
- E-Commerce HOT 1
- [React 19] HOT 1
- [React 19] Eslint React JSDoc support HOT 1
- [React 19] Support scoped custom element registries (i.e, react with Custom Elements being rendered in a shadow root) HOT 1
- Bug [React-DOM]: Missing "bun" export field in package.json HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from react.