Skip to content

Commit 5a602d5

Browse files
committed
Fix: flushSync changes priority inside effect (facebook#21122)
When called from inside an effect, flushSync cannot synchronously flush its updates because React is already working. So we fire a warning. However, we should still change the priority of the updates to sync so that they flush at the end of the current task. This only affects useEffect because updates inside useLayoutEffect (and the rest of the commit phase, like ref callbacks) are already sync.
1 parent ee9ff3e commit 5a602d5

File tree

3 files changed

+79
-22
lines changed

3 files changed

+79
-22
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+11-11
Original file line numberDiff line numberDiff line change
@@ -1161,16 +1161,6 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
11611161

11621162
export function flushSync<A, R>(fn: A => R, a: A): R {
11631163
const prevExecutionContext = executionContext;
1164-
if ((prevExecutionContext & (RenderContext | CommitContext)) !== NoContext) {
1165-
if (__DEV__) {
1166-
console.error(
1167-
'flushSync was called from inside a lifecycle method. React cannot ' +
1168-
'flush when React is already rendering. Consider moving this call to ' +
1169-
'a scheduler task or micro task.',
1170-
);
1171-
}
1172-
return fn(a);
1173-
}
11741164
executionContext |= BatchedContext;
11751165

11761166
const previousPriority = getCurrentUpdatePriority();
@@ -1187,7 +1177,17 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11871177
// Flush the immediate callbacks that were scheduled during this batch.
11881178
// Note that this will happen even if batchedUpdates is higher up
11891179
// the stack.
1190-
flushSyncCallbackQueue();
1180+
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1181+
flushSyncCallbackQueue();
1182+
} else {
1183+
if (__DEV__) {
1184+
console.error(
1185+
'flushSync was called from inside a lifecycle method. React cannot ' +
1186+
'flush when React is already rendering. Consider moving this call to ' +
1187+
'a scheduler task or micro task.',
1188+
);
1189+
}
1190+
}
11911191
}
11921192
}
11931193

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

+11-11
Original file line numberDiff line numberDiff line change
@@ -1161,16 +1161,6 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
11611161

11621162
export function flushSync<A, R>(fn: A => R, a: A): R {
11631163
const prevExecutionContext = executionContext;
1164-
if ((prevExecutionContext & (RenderContext | CommitContext)) !== NoContext) {
1165-
if (__DEV__) {
1166-
console.error(
1167-
'flushSync was called from inside a lifecycle method. React cannot ' +
1168-
'flush when React is already rendering. Consider moving this call to ' +
1169-
'a scheduler task or micro task.',
1170-
);
1171-
}
1172-
return fn(a);
1173-
}
11741164
executionContext |= BatchedContext;
11751165

11761166
const previousPriority = getCurrentUpdatePriority();
@@ -1187,7 +1177,17 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11871177
// Flush the immediate callbacks that were scheduled during this batch.
11881178
// Note that this will happen even if batchedUpdates is higher up
11891179
// the stack.
1190-
flushSyncCallbackQueue();
1180+
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1181+
flushSyncCallbackQueue();
1182+
} else {
1183+
if (__DEV__) {
1184+
console.error(
1185+
'flushSync was called from inside a lifecycle method. React cannot ' +
1186+
'flush when React is already rendering. Consider moving this call to ' +
1187+
'a scheduler task or micro task.',
1188+
);
1189+
}
1190+
}
11911191
}
11921192
}
11931193

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
let React;
2+
let ReactNoop;
3+
let Scheduler;
4+
let useState;
5+
let useEffect;
6+
7+
describe('ReactFlushSync', () => {
8+
beforeEach(() => {
9+
jest.resetModules();
10+
11+
React = require('react');
12+
ReactNoop = require('react-noop-renderer');
13+
Scheduler = require('scheduler');
14+
useState = React.useState;
15+
useEffect = React.useEffect;
16+
});
17+
18+
function Text({text}) {
19+
Scheduler.unstable_yieldValue(text);
20+
return text;
21+
}
22+
23+
test('changes priority of updates in useEffect', async () => {
24+
function App() {
25+
const [syncState, setSyncState] = useState(0);
26+
const [state, setState] = useState(0);
27+
useEffect(() => {
28+
if (syncState !== 1) {
29+
setState(1);
30+
ReactNoop.flushSync(() => setSyncState(1));
31+
}
32+
}, [syncState, state]);
33+
return <Text text={`${syncState}, ${state}`} />;
34+
}
35+
36+
const root = ReactNoop.createRoot();
37+
await ReactNoop.act(async () => {
38+
root.render(<App />);
39+
// This will yield right before the passive effect fires
40+
expect(Scheduler).toFlushUntilNextPaint(['0, 0']);
41+
42+
// The passive effect will schedule a sync update and a normal update.
43+
// They should commit in two separate batches. First the sync one.
44+
expect(() =>
45+
expect(Scheduler).toFlushUntilNextPaint(['1, 0']),
46+
).toErrorDev('flushSync was called from inside a lifecycle method');
47+
48+
// The remaining update is not sync
49+
ReactNoop.flushSync();
50+
expect(Scheduler).toHaveYielded([]);
51+
52+
// Now flush it.
53+
expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
54+
});
55+
expect(root).toMatchRenderedOutput('1, 1');
56+
});
57+
});

0 commit comments

Comments
 (0)