Compare commits
9 Commits
main
...
ty-rh-defa
| Author | SHA1 | Date |
|---|---|---|
|
|
6a82e515f9 | |
|
|
079f792b4e | |
|
|
39b4cc87d9 | |
|
|
1cba37e4ae | |
|
|
16e3268f1f | |
|
|
eb583f0663 | |
|
|
ec78a39f65 | |
|
|
e5ac893ab8 | |
|
|
128e7c4cf3 |
|
|
@ -106,6 +106,11 @@ export function act<T>(scope: () => Thenable<T> | T): Thenable<T> {
|
|||
let didFlushWork;
|
||||
do {
|
||||
didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
|
||||
|
||||
// Flush scheduled rAF.
|
||||
if (global.flushRequestAnimationFrameQueue) {
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
}
|
||||
} while (didFlushWork);
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
|
|
@ -130,6 +135,11 @@ function flushActWork(resolve, reject) {
|
|||
reject(error);
|
||||
}
|
||||
|
||||
// Flush scheduled rAF.
|
||||
if (global.flushRequestAnimationFrameQueue) {
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
}
|
||||
|
||||
// If Scheduler yields while there's still work, it's so that we can
|
||||
// unblock the main thread (e.g. for paint or for microtasks). Yield to
|
||||
// the main thread and continue in a new task.
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
|
|||
|
||||
// TODO: Remove this deep import when we delete the legacy root API
|
||||
import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
|
||||
import * as Scheduler from 'scheduler';
|
||||
|
||||
import {
|
||||
prepareToRenderResources,
|
||||
|
|
@ -430,6 +431,11 @@ export function getInstanceFromScope(
|
|||
return null;
|
||||
}
|
||||
|
||||
const localCancelAnimationFrame =
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.cancelAnimationFrame === 'function'
|
||||
? window.cancelAnimationFrame
|
||||
: cancelTimeout;
|
||||
// -------------------
|
||||
// Microtasks
|
||||
// -------------------
|
||||
|
|
@ -445,6 +451,65 @@ export const scheduleMicrotask: any =
|
|||
.catch(handleErrorInNextTick)
|
||||
: scheduleTimeout; // TODO: Determine the best fallback here.
|
||||
|
||||
export const supportsFrameAlignedTask = true;
|
||||
|
||||
type FrameAlignedTask = {|
|
||||
rafNode: AnimationFrameID,
|
||||
schedulerNode: number | null,
|
||||
task: function,
|
||||
|};
|
||||
|
||||
let currentTask: FrameAlignedTask | null = null;
|
||||
function performFrameAlignedWork() {
|
||||
if (currentTask != null) {
|
||||
const currentTaskForFlow = currentTask;
|
||||
const task = currentTask.task;
|
||||
localCancelAnimationFrame(currentTaskForFlow.rafNode);
|
||||
if (currentTaskForFlow.schedulerNode !== null) {
|
||||
Scheduler.unstable_cancelCallback(currentTaskForFlow.schedulerNode);
|
||||
}
|
||||
currentTask = null;
|
||||
if (task != null) {
|
||||
task();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function scheduleFrameAlignedTask(task: any): any {
|
||||
if (currentTask === null) {
|
||||
const rafNode = localRequestAnimationFrame(performFrameAlignedWork);
|
||||
|
||||
const schedulerNode = Scheduler.unstable_scheduleCallback(
|
||||
Scheduler.unstable_NormalPriority,
|
||||
performFrameAlignedWork,
|
||||
);
|
||||
|
||||
currentTask = {
|
||||
rafNode,
|
||||
schedulerNode,
|
||||
task,
|
||||
};
|
||||
} else {
|
||||
currentTask.task = task;
|
||||
currentTask.schedulerNode = Scheduler.unstable_scheduleCallback(
|
||||
Scheduler.unstable_NormalPriority,
|
||||
performFrameAlignedWork,
|
||||
);
|
||||
}
|
||||
|
||||
return currentTask;
|
||||
}
|
||||
|
||||
export function cancelFrameAlignedTask(task: any) {
|
||||
if (task.schedulerNode) {
|
||||
Scheduler.unstable_cancelCallback(task.schedulerNode);
|
||||
task.schedulerNode = null;
|
||||
}
|
||||
// We don't cancel the rAF in case it gets re-used later.
|
||||
// But clear the task so if it fires and shouldn't run, it won't.
|
||||
task.task = null;
|
||||
}
|
||||
|
||||
function handleErrorInNextTick(error) {
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ describe('ReactDOMFiberAsync', () => {
|
|||
|
||||
handleChange = e => {
|
||||
const nextValue = e.target.value;
|
||||
requestIdleCallback(() => {
|
||||
React.startTransition(() => {
|
||||
this.setState({
|
||||
asyncValue: nextValue,
|
||||
});
|
||||
|
|
@ -275,17 +275,32 @@ describe('ReactDOMFiberAsync', () => {
|
|||
expect(ops).toEqual([]);
|
||||
});
|
||||
// Only the active updates have flushed
|
||||
expect(container.textContent).toEqual('BC');
|
||||
expect(ops).toEqual(['BC']);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(container.textContent).toEqual('ABC');
|
||||
expect(ops).toEqual(['ABC']);
|
||||
} else {
|
||||
expect(container.textContent).toEqual('BC');
|
||||
expect(ops).toEqual(['BC']);
|
||||
}
|
||||
|
||||
instance.push('D');
|
||||
expect(container.textContent).toEqual('BC');
|
||||
expect(ops).toEqual(['BC']);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
instance.push('D');
|
||||
expect(container.textContent).toEqual('ABC');
|
||||
expect(ops).toEqual(['ABC']);
|
||||
} else {
|
||||
instance.push('D');
|
||||
expect(container.textContent).toEqual('BC');
|
||||
expect(ops).toEqual(['BC']);
|
||||
}
|
||||
|
||||
// Flush the async updates
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('ABCD');
|
||||
expect(ops).toEqual(['BC', 'ABCD']);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(ops).toEqual(['ABC', 'ABCD']);
|
||||
} else {
|
||||
expect(ops).toEqual(['BC', 'ABCD']);
|
||||
}
|
||||
});
|
||||
|
||||
// @gate www
|
||||
|
|
@ -545,6 +560,392 @@ describe('ReactDOMFiberAsync', () => {
|
|||
// Therefore the form should have been submitted.
|
||||
expect(formSubmitted).toBe(true);
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling
|
||||
it('Unknown update followed by default update is batched, scheduled in a rAF', async () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return <p ref={ref}>Count: {count}</p>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = undefined;
|
||||
setState(1);
|
||||
// Unknown updates should schedule a rAF.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
|
||||
window.event = 'test';
|
||||
setState(2);
|
||||
// Default updates after unknown should re-use the scheduled rAF.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(counterRef.current.textContent).toBe('Count: 0');
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
expect(Scheduler).toHaveYielded(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling
|
||||
it('Unknown update followed by default update is batched, scheduled in a task', () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return <p ref={ref}>Count: {count}</p>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
act(() => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = undefined;
|
||||
setState(1);
|
||||
// Unknown updates should schedule a rAF.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
|
||||
window.event = 'test';
|
||||
setState(2);
|
||||
// Default updates after unknown should re-use the scheduled rAF.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(counterRef.current.textContent).toBe('Count: 0');
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling
|
||||
it('Should re-use scheduled rAF, not cancel and schedule anew', async () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return <p ref={ref}>Count: {count}</p>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = undefined;
|
||||
setState(1);
|
||||
// Unknown updates should schedule a rAF.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
const firstRaf = global.requestAnimationFrameQueue[0];
|
||||
|
||||
setState(2);
|
||||
// Default updates after unknown should re-use the scheduled rAF.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
const secondRaf = global.requestAnimationFrameQueue[0];
|
||||
expect(firstRaf).toBe(secondRaf);
|
||||
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(counterRef.current.textContent).toBe('Count: 0');
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
expect(Scheduler).toHaveYielded(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling
|
||||
it('Default update followed by an unknown update is batched, scheduled in a rAF', async () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return <p ref={ref}>Count: {count}</p>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = 'test';
|
||||
setState(1);
|
||||
|
||||
// We should schedule a rAF for default updates.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
|
||||
window.event = undefined;
|
||||
setState(2);
|
||||
// Unknown updates should schedule a rAF.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(counterRef.current.textContent).toBe('Count: 0');
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
expect(Scheduler).toHaveYielded(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling
|
||||
it('Default update followed by unknown update is batched, scheduled in a task', async () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return <p ref={ref}>Count: {count}</p>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = 'test';
|
||||
setState(1);
|
||||
|
||||
// We should schedule a rAF for default updates.
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
|
||||
window.event = undefined;
|
||||
setState(2);
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(counterRef.current.textContent).toBe('Count: 0');
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling || !allowConcurrentByDefault
|
||||
it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced', async () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return <p ref={ref}>Count: {count}</p>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = undefined;
|
||||
setState(1);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Count: 1']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 1');
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling || !allowConcurrentByDefault
|
||||
it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced event with default first', async () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return <p ref={ref}>Count: {count}</p>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = 'test';
|
||||
setState(1);
|
||||
|
||||
window.event = undefined;
|
||||
setState(2);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling || !allowConcurrentByDefault
|
||||
it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced event with default after', async () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return <p ref={ref}>Count: {count}</p>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = undefined;
|
||||
setState(1);
|
||||
|
||||
window.event = 'test';
|
||||
setState(2);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling
|
||||
it('unknown updates should be rescheduled in rAF after a higher priority update', async () => {
|
||||
let setState = null;
|
||||
let counterRef = null;
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
setCount(c => c + 1);
|
||||
}}>
|
||||
Count: {count}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = undefined;
|
||||
setState(1);
|
||||
|
||||
// Dispatch a click event on the button.
|
||||
await act(async () => {
|
||||
const firstEvent = document.createEvent('Event');
|
||||
firstEvent.initEvent('click', true, true);
|
||||
counterRef.current.dispatchEvent(firstEvent);
|
||||
});
|
||||
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toHaveYielded(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
} else {
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 1');
|
||||
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
expect(Scheduler).toHaveYielded(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
}
|
||||
});
|
||||
|
||||
// @gate enableFrameEndScheduling && enableUnifiedSyncLane
|
||||
it('unknown updates should be rescheduled in rAF after suspending without a boundary', async () => {
|
||||
let setState = null;
|
||||
let setThrowing = null;
|
||||
let counterRef = null;
|
||||
|
||||
let promise = null;
|
||||
let unsuspend = null;
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const [isThrowing, setThrowingState] = React.useState(false);
|
||||
setThrowing = setThrowingState;
|
||||
const ref = React.useRef();
|
||||
setState = setCount;
|
||||
counterRef = ref;
|
||||
Scheduler.unstable_yieldValue('Count: ' + count);
|
||||
if (isThrowing) {
|
||||
if (promise === null) {
|
||||
promise = new Promise(resolve => {
|
||||
unsuspend = () => {
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
Scheduler.unstable_yieldValue('suspending');
|
||||
throw promise;
|
||||
}
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
setCount(c => c + 1);
|
||||
}}>
|
||||
Count: {count}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<Counter />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 0']);
|
||||
|
||||
window.event = undefined;
|
||||
setState(1);
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
|
||||
setState(2);
|
||||
setThrowing(true);
|
||||
expect(global.requestAnimationFrameQueue.length).toBe(1);
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
expect(Scheduler).toHaveYielded(['Count: 2', 'suspending']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 1');
|
||||
|
||||
unsuspend();
|
||||
// Default update should be scheduled in a rAF.
|
||||
window.event = 'test';
|
||||
setThrowing(false);
|
||||
setState(2);
|
||||
|
||||
global.flushRequestAnimationFrameQueue();
|
||||
expect(Scheduler).toHaveYielded(['Count: 2']);
|
||||
expect(counterRef.current.textContent).toBe('Count: 2');
|
||||
});
|
||||
});
|
||||
|
||||
it('regression test: does not drop passive effects across roots (#17066)', () => {
|
||||
|
|
|
|||
|
|
@ -397,6 +397,9 @@ describe('ReactDOMRoot', () => {
|
|||
|
||||
expect(container.textContent).toEqual('a');
|
||||
|
||||
// Set an event so this isn't flushed synchronously as an unknown update.
|
||||
window.event = 'test';
|
||||
|
||||
await act(async () => {
|
||||
root.render(<Foo value="b" />);
|
||||
|
||||
|
|
@ -404,7 +407,12 @@ describe('ReactDOMRoot', () => {
|
|||
expect(container.textContent).toEqual('a');
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['b']);
|
||||
if (gate(flags => flags.allowConcurrentByDefault)) {
|
||||
if (
|
||||
gate(
|
||||
flags =>
|
||||
flags.allowConcurrentByDefault && !flags.enableFrameEndScheduling,
|
||||
)
|
||||
) {
|
||||
expect(container.textContent).toEqual('a');
|
||||
} else {
|
||||
expect(container.textContent).toEqual('b');
|
||||
|
|
|
|||
|
|
@ -449,10 +449,9 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
expect(deleted.length).toBe(0);
|
||||
|
||||
// Performing an update should force it to delete the boundary
|
||||
root.render(<App value={true} />);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
root.render(<App value={true} />);
|
||||
});
|
||||
|
||||
expect(hydrated.length).toBe(1);
|
||||
expect(deleted.length).toBe(1);
|
||||
|
|
@ -945,13 +944,12 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
root.render(<App text="Hi" className="hi" />);
|
||||
|
||||
// At the same time, resolving the promise so that rendering can complete.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
// This should first complete the hydration and then flush the update onto the hydrated state.
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
});
|
||||
|
||||
// The new span should be the same since we should have successfully hydrated
|
||||
// before changing it.
|
||||
|
|
@ -1093,9 +1091,9 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
expect(ref.current).toBe(null);
|
||||
|
||||
// Render an update, but leave it still suspended.
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
});
|
||||
|
||||
// Flushing now should delete the existing content and show the fallback.
|
||||
|
||||
|
|
@ -1104,12 +1102,11 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// Unsuspending shows the content.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
});
|
||||
|
||||
const span = container.getElementsByTagName('span')[0];
|
||||
expect(span.textContent).toBe('Hi');
|
||||
|
|
@ -1174,23 +1171,21 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
expect(ref.current).toBe(span);
|
||||
|
||||
// Render an update, but leave it still suspended.
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
|
||||
// Flushing now should delete the existing content and show the fallback.
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
});
|
||||
|
||||
expect(container.getElementsByTagName('span').length).toBe(1);
|
||||
expect(ref.current).toBe(span);
|
||||
expect(container.textContent).toBe('');
|
||||
|
||||
// Unsuspending shows the content.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
});
|
||||
|
||||
expect(span.textContent).toBe('Hi');
|
||||
expect(span.className).toBe('hi');
|
||||
|
|
@ -1252,20 +1247,21 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
expect(ref.current).toBe(null);
|
||||
|
||||
// Render an update, but leave it still suspended.
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
|
||||
// Flushing now should delete the existing content and show the fallback.
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
root.render(<App text="Hi" className="hi" />);
|
||||
});
|
||||
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
expect(ref.current).toBe(null);
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// Unsuspending shows the content.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
await act(async () => {
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
});
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
|
|
@ -1408,12 +1404,16 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// While we're part way through the hydration, we update the state.
|
||||
// This will schedule an update on the children of the suspense boundary.
|
||||
expect(() => updateText('Hi')).toErrorDev(
|
||||
expect(() => {
|
||||
act(() => {
|
||||
updateText('Hi');
|
||||
});
|
||||
}).toErrorDev(
|
||||
"Can't perform a React state update on a component that hasn't mounted yet.",
|
||||
);
|
||||
|
||||
// This will throw it away and rerender.
|
||||
expect(Scheduler).toFlushAndYield(['Child', 'Sibling']);
|
||||
expect(Scheduler).toHaveYielded(['Child', 'Sibling']);
|
||||
|
||||
expect(container.textContent).toBe('Hello');
|
||||
|
||||
|
|
@ -1490,13 +1490,12 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
);
|
||||
|
||||
// At the same time, resolving the promise so that rendering can complete.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
// This should first complete the hydration and then flush the update onto the hydrated state.
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
});
|
||||
|
||||
// Since this should have been hydrated, this should still be the same span.
|
||||
const newSpan = container.getElementsByTagName('span')[0];
|
||||
|
|
@ -1569,27 +1568,25 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
expect(ref.current).toBe(null);
|
||||
|
||||
// Render an update, but leave it still suspended.
|
||||
root.render(
|
||||
<Context.Provider value={{text: 'Hi', className: 'hi'}}>
|
||||
<App />
|
||||
</Context.Provider>,
|
||||
);
|
||||
|
||||
// Flushing now should delete the existing content and show the fallback.
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Context.Provider value={{text: 'Hi', className: 'hi'}}>
|
||||
<App />
|
||||
</Context.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(container.getElementsByTagName('span').length).toBe(0);
|
||||
expect(ref.current).toBe(null);
|
||||
expect(container.textContent).toBe('Loading...');
|
||||
|
||||
// Unsuspending shows the content.
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
});
|
||||
|
||||
const span = container.getElementsByTagName('span')[0];
|
||||
expect(span.textContent).toBe('Hi');
|
||||
|
|
@ -2320,16 +2317,15 @@ describe('ReactDOMServerPartialHydration', () => {
|
|||
|
||||
// Render an update, which will be higher or the same priority as pinging the hydration.
|
||||
// The new update doesn't suspend.
|
||||
root.render(
|
||||
<ClassName.Provider value={'hi'}>
|
||||
<App text="Hi" />
|
||||
</ClassName.Provider>,
|
||||
);
|
||||
|
||||
// Since we're still suspended on the original data, we can't hydrate.
|
||||
// This will force all expiration times to flush.
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<ClassName.Provider value={'hi'}>
|
||||
<App text="Hi" />
|
||||
</ClassName.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
// This will now be a new span because we weren't able to hydrate before
|
||||
const newSpan = container.getElementsByTagName('span')[0];
|
||||
|
|
|
|||
|
|
@ -1786,7 +1786,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
it('can force hydration in response to sync update', () => {
|
||||
it('can force hydration in response to sync update', async () => {
|
||||
function Child({text}) {
|
||||
Scheduler.unstable_yieldValue(`Child ${text}`);
|
||||
return <span ref={ref => (spanRef = ref)}>{text}</span>;
|
||||
|
|
@ -1812,15 +1812,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
|
||||
expect(Scheduler).toFlushUntilNextPaint(['App A']);
|
||||
|
||||
ReactDOM.flushSync(() => {
|
||||
root.render(<App text="B" />);
|
||||
await act(async () => {
|
||||
ReactDOM.flushSync(() => {
|
||||
root.render(<App text="B" />);
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
|
||||
expect(initialSpan).toBe(spanRef);
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
it('can force hydration in response to continuous update', () => {
|
||||
it('can force hydration in response to continuous update', async () => {
|
||||
function Child({text}) {
|
||||
Scheduler.unstable_yieldValue(`Child ${text}`);
|
||||
return <span ref={ref => (spanRef = ref)}>{text}</span>;
|
||||
|
|
@ -1846,14 +1848,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
|
||||
expect(Scheduler).toFlushUntilNextPaint(['App A']);
|
||||
|
||||
TODO_scheduleContinuousSchedulerTask(() => {
|
||||
root.render(<App text="B" />);
|
||||
await act(async () => {
|
||||
TODO_scheduleContinuousSchedulerTask(() => {
|
||||
root.render(<App text="B" />);
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['App B', 'Child A', 'App B', 'Child B']);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
|
||||
expect(initialSpan).toBe(spanRef);
|
||||
});
|
||||
|
||||
it('can force hydration in response to default update', () => {
|
||||
it('can force hydration in response to default update', async () => {
|
||||
function Child({text}) {
|
||||
Scheduler.unstable_yieldValue(`Child ${text}`);
|
||||
return <span ref={ref => (spanRef = ref)}>{text}</span>;
|
||||
|
|
@ -1878,11 +1883,10 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
|||
const initialSpan = container.getElementsByTagName('span')[0];
|
||||
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
|
||||
expect(Scheduler).toFlushUntilNextPaint(['App A']);
|
||||
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
await act(async () => {
|
||||
root.render(<App text="B" />);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['App B', 'Child A', 'App B', 'Child B']);
|
||||
expect(Scheduler).toHaveYielded(['App B', 'Child A', 'App B', 'Child B']);
|
||||
expect(initialSpan).toBe(spanRef);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -139,24 +139,24 @@ describe('useId', () => {
|
|||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="1001"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="1001"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('indirections', async () => {
|
||||
|
|
@ -184,24 +184,24 @@ describe('useId', () => {
|
|||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="0"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
id="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="0"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
id="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('StrictMode double rendering', async () => {
|
||||
|
|
@ -223,14 +223,14 @@ describe('useId', () => {
|
|||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="0"
|
||||
/>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="0"
|
||||
/>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('empty (null) children', async () => {
|
||||
|
|
@ -259,17 +259,17 @@ describe('useId', () => {
|
|||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="10"
|
||||
/>
|
||||
<div
|
||||
id="100"
|
||||
/>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="10"
|
||||
/>
|
||||
<div
|
||||
id="100"
|
||||
/>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('large ids', async () => {
|
||||
|
|
@ -339,12 +339,12 @@ describe('useId', () => {
|
|||
});
|
||||
// We append a suffix to the end of the id to distinguish them
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
:R0:, :R0H1:, :R0H2:
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
:R0:, :R0H1:, :R0H2:
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('local render phase updates', async () => {
|
||||
|
|
@ -364,12 +364,12 @@ describe('useId', () => {
|
|||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
:R0:
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
:R0:
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('basic incremental hydration', async () => {
|
||||
|
|
@ -393,24 +393,24 @@ describe('useId', () => {
|
|||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
<!--$-->
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="1001"
|
||||
/>
|
||||
<!--/$-->
|
||||
<div
|
||||
id="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
<!--$-->
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="1001"
|
||||
/>
|
||||
<!--/$-->
|
||||
<div
|
||||
id="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('inserting/deleting siblings outside a dehydrated Suspense boundary', async () => {
|
||||
|
|
@ -444,26 +444,26 @@ describe('useId', () => {
|
|||
const root = ReactDOMClient.hydrateRoot(container, <App />);
|
||||
expect(Scheduler).toFlushUntilNextPaint([]);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="1001"
|
||||
/>
|
||||
<div
|
||||
id="1101"
|
||||
/>
|
||||
<!--$-->
|
||||
<div
|
||||
id="110"
|
||||
/>
|
||||
<span />
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="1001"
|
||||
/>
|
||||
<div
|
||||
id="1101"
|
||||
/>
|
||||
<!--$-->
|
||||
<div
|
||||
id="110"
|
||||
/>
|
||||
<span />
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
|
||||
// The inner boundary hasn't hydrated yet
|
||||
expect(span.current).toBe(null);
|
||||
|
|
@ -473,26 +473,26 @@ describe('useId', () => {
|
|||
});
|
||||
// The swap should not have caused a mismatch.
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="CLIENT_GENERATED_ID"
|
||||
/>
|
||||
<div
|
||||
id="1101"
|
||||
/>
|
||||
<!--$-->
|
||||
<div
|
||||
id="110"
|
||||
/>
|
||||
<span />
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="CLIENT_GENERATED_ID"
|
||||
/>
|
||||
<div
|
||||
id="1101"
|
||||
/>
|
||||
<!--$-->
|
||||
<div
|
||||
id="110"
|
||||
/>
|
||||
<span />
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
// Should have hydrated successfully
|
||||
expect(span.current).toBe(dehydratedSpan);
|
||||
});
|
||||
|
|
@ -525,23 +525,23 @@ describe('useId', () => {
|
|||
const root = ReactDOMClient.hydrateRoot(container, <App />);
|
||||
expect(Scheduler).toFlushUntilNextPaint([]);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="1001"
|
||||
/>
|
||||
<div
|
||||
id="1101"
|
||||
/>
|
||||
<span />
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="1001"
|
||||
/>
|
||||
<div
|
||||
id="1101"
|
||||
/>
|
||||
<span />
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
|
||||
// The inner boundary hasn't hydrated yet
|
||||
expect(span.current).toBe(null);
|
||||
|
|
@ -551,23 +551,23 @@ describe('useId', () => {
|
|||
});
|
||||
// The swap should not have caused a mismatch.
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="CLIENT_GENERATED_ID"
|
||||
/>
|
||||
<div
|
||||
id="1101"
|
||||
/>
|
||||
<span />
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
id="101"
|
||||
/>
|
||||
<div
|
||||
id="CLIENT_GENERATED_ID"
|
||||
/>
|
||||
<div
|
||||
id="1101"
|
||||
/>
|
||||
<span />
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
// Should have hydrated successfully
|
||||
expect(span.current).toBe(dehydratedSpan);
|
||||
});
|
||||
|
|
@ -601,37 +601,37 @@ describe('useId', () => {
|
|||
});
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
:custom-prefix-R1:
|
||||
</div>
|
||||
<div>
|
||||
:custom-prefix-R2:
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
:custom-prefix-R1:
|
||||
</div>
|
||||
<div>
|
||||
:custom-prefix-R2:
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Mount a new, client-only id
|
||||
await clientAct(async () => {
|
||||
root.render(<App showMore={true} />);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
:custom-prefix-R1:
|
||||
</div>
|
||||
<div>
|
||||
:custom-prefix-R2:
|
||||
</div>
|
||||
<div>
|
||||
:custom-prefix-r0:
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
:custom-prefix-R1:
|
||||
</div>
|
||||
<div>
|
||||
:custom-prefix-R2:
|
||||
</div>
|
||||
<div>
|
||||
:custom-prefix-r0:
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// https://github.com/vercel/next.js/issues/43033
|
||||
|
|
@ -665,36 +665,36 @@ describe('useId', () => {
|
|||
pipe(writable);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
:R0:
|
||||
<!-- -->
|
||||
|
||||
<div>
|
||||
:R7:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
:R0:
|
||||
<!-- -->
|
||||
|
||||
<div>
|
||||
:R7:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
await clientAct(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
:R0:
|
||||
<!-- -->
|
||||
|
||||
<div>
|
||||
:R7:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
<div
|
||||
id="container"
|
||||
>
|
||||
<div>
|
||||
:R0:
|
||||
<!-- -->
|
||||
|
||||
<div>
|
||||
:R7:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -432,6 +432,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
})
|
||||
: setTimeout,
|
||||
|
||||
supportsFrameAlignedTask: false,
|
||||
scheduleFrameAlignedTask: undefined,
|
||||
cancelFrameAlignedTask: undefined,
|
||||
|
||||
prepareForCommit(): null | Object {
|
||||
return null;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -206,6 +206,17 @@ Set this to true to indicate that your renderer supports `scheduleMicrotask`. We
|
|||
|
||||
Optional. You can proxy this to `queueMicrotask` or its equivalent in your environment.
|
||||
|
||||
#### `supportsFrameAlignedTask`
|
||||
TODO
|
||||
|
||||
### `scheduleFrameAlignedTask(fn)`
|
||||
|
||||
TODO
|
||||
|
||||
#### `cancelFrameAlignedTask(fn)`
|
||||
|
||||
TODO
|
||||
|
||||
#### `isPrimaryRenderer`
|
||||
|
||||
This is a property (not a function) that should be set to `true` if your renderer is the main one on the page. For example, if you're writing a renderer for the Terminal, it makes sense to set it to `true`, but if your renderer is used *on top of* React DOM or some other existing renderer, set it to `false`.
|
||||
|
|
|
|||
|
|
@ -1461,7 +1461,6 @@ function useMutableSource<Source, Snapshot>(
|
|||
|
||||
// Record a pending mutable source update with the same expiration time.
|
||||
const lane = requestUpdateLane(fiber);
|
||||
|
||||
markRootMutableRead(root, lane);
|
||||
} catch (error) {
|
||||
// A selector might throw after a source mutation.
|
||||
|
|
|
|||
|
|
@ -21,3 +21,6 @@ function shim(...args: any): empty {
|
|||
// Test selectors (when unsupported)
|
||||
export const supportsMicrotasks = false;
|
||||
export const scheduleMicrotask = shim;
|
||||
export const supportsFrameAlignedTask = false;
|
||||
export const scheduleFrameAlignedTask = shim;
|
||||
export const cancelFrameAlignedTask = shim;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import {
|
|||
enableUpdaterTracking,
|
||||
allowConcurrentByDefault,
|
||||
enableTransitionTracing,
|
||||
enableUnifiedSyncLane,
|
||||
enableFrameEndScheduling,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
|
||||
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
|
||||
|
|
@ -45,6 +47,8 @@ export const InputContinuousLane: Lane = /* */ 0b0000000000000000000
|
|||
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
|
||||
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
|
||||
|
||||
export const SyncUpdateLanes: Lane = /* */ 0b0000000000000000000000000101010;
|
||||
|
||||
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
|
||||
const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;
|
||||
const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
|
||||
|
|
@ -133,6 +137,12 @@ let nextTransitionLane: Lane = TransitionLane1;
|
|||
let nextRetryLane: Lane = RetryLane1;
|
||||
|
||||
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
|
||||
if (enableUnifiedSyncLane) {
|
||||
const pendingSyncLanes = lanes & SyncUpdateLanes;
|
||||
if (pendingSyncLanes !== 0) {
|
||||
return pendingSyncLanes;
|
||||
}
|
||||
}
|
||||
switch (getHighestPriorityLane(lanes)) {
|
||||
case SyncHydrationLane:
|
||||
return SyncHydrationLane;
|
||||
|
|
@ -251,7 +261,10 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
|
|||
// Default priority updates should not interrupt transition updates. The
|
||||
// only difference between default updates and transition updates is that
|
||||
// default updates do not support refresh transitions.
|
||||
(nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
|
||||
// Interrupt transtion if default is batched with sync.
|
||||
(!enableUnifiedSyncLane &&
|
||||
nextLane === DefaultLane &&
|
||||
(wipLane & TransitionLanes) !== NoLanes)
|
||||
) {
|
||||
// Keep working on the existing in-progress tree. Do not interrupt.
|
||||
return wipLanes;
|
||||
|
|
@ -487,7 +500,12 @@ export function includesBlockingLane(root: FiberRoot, lanes: Lanes): boolean {
|
|||
allowConcurrentByDefault &&
|
||||
(root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
|
||||
) {
|
||||
// Concurrent updates by default always use time slicing.
|
||||
if (enableFrameEndScheduling && (lanes & DefaultLane) !== NoLanes) {
|
||||
// Unknown updates should flush synchronously, even in concurrent by default.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, concurrent updates by default always use time slicing.
|
||||
return false;
|
||||
}
|
||||
const SyncDefaultLanes =
|
||||
|
|
@ -754,46 +772,53 @@ export function getBumpedLaneForHydration(
|
|||
const renderLane = getHighestPriorityLane(renderLanes);
|
||||
|
||||
let lane;
|
||||
switch (renderLane) {
|
||||
case SyncLane:
|
||||
if (enableUnifiedSyncLane) {
|
||||
if ((renderLane & SyncUpdateLanes) !== NoLane) {
|
||||
lane = SyncHydrationLane;
|
||||
break;
|
||||
case InputContinuousLane:
|
||||
lane = InputContinuousHydrationLane;
|
||||
break;
|
||||
case DefaultLane:
|
||||
lane = DefaultHydrationLane;
|
||||
break;
|
||||
case TransitionLane1:
|
||||
case TransitionLane2:
|
||||
case TransitionLane3:
|
||||
case TransitionLane4:
|
||||
case TransitionLane5:
|
||||
case TransitionLane6:
|
||||
case TransitionLane7:
|
||||
case TransitionLane8:
|
||||
case TransitionLane9:
|
||||
case TransitionLane10:
|
||||
case TransitionLane11:
|
||||
case TransitionLane12:
|
||||
case TransitionLane13:
|
||||
case TransitionLane14:
|
||||
case TransitionLane15:
|
||||
case TransitionLane16:
|
||||
case RetryLane1:
|
||||
case RetryLane2:
|
||||
case RetryLane3:
|
||||
case RetryLane4:
|
||||
lane = TransitionHydrationLane;
|
||||
break;
|
||||
case IdleLane:
|
||||
lane = IdleHydrationLane;
|
||||
break;
|
||||
default:
|
||||
// Everything else is already either a hydration lane, or shouldn't
|
||||
// be retried at a hydration lane.
|
||||
lane = NoLane;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!lane) {
|
||||
switch (renderLane) {
|
||||
case SyncLane:
|
||||
lane = SyncHydrationLane;
|
||||
break;
|
||||
case InputContinuousLane:
|
||||
lane = InputContinuousHydrationLane;
|
||||
break;
|
||||
case DefaultLane:
|
||||
lane = DefaultHydrationLane;
|
||||
break;
|
||||
case TransitionLane1:
|
||||
case TransitionLane2:
|
||||
case TransitionLane3:
|
||||
case TransitionLane4:
|
||||
case TransitionLane5:
|
||||
case TransitionLane6:
|
||||
case TransitionLane7:
|
||||
case TransitionLane8:
|
||||
case TransitionLane9:
|
||||
case TransitionLane10:
|
||||
case TransitionLane11:
|
||||
case TransitionLane12:
|
||||
case TransitionLane13:
|
||||
case TransitionLane14:
|
||||
case TransitionLane15:
|
||||
case TransitionLane16:
|
||||
case RetryLane1:
|
||||
case RetryLane2:
|
||||
case RetryLane3:
|
||||
case RetryLane4:
|
||||
lane = TransitionHydrationLane;
|
||||
break;
|
||||
case IdleLane:
|
||||
lane = IdleHydrationLane;
|
||||
break;
|
||||
default:
|
||||
// Everything else is already either a hydration lane, or shouldn't
|
||||
// be retried at a hydration lane.
|
||||
lane = NoLane;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the lane we chose is suspended. If so, that indicates that we
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
enableUpdaterTracking,
|
||||
enableCache,
|
||||
enableTransitionTracing,
|
||||
enableFrameEndScheduling,
|
||||
useModernStrictMode,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
|
|
@ -85,8 +86,10 @@ import {
|
|||
scheduleMicrotask,
|
||||
prepareRendererToRender,
|
||||
resetRendererAfterRender,
|
||||
cancelFrameAlignedTask,
|
||||
scheduleFrameAlignedTask,
|
||||
supportsFrameAlignedTask,
|
||||
} from './ReactFiberHostConfig';
|
||||
|
||||
import {
|
||||
createWorkInProgress,
|
||||
assignFiberPropertiesInDEV,
|
||||
|
|
@ -162,6 +165,7 @@ import {
|
|||
movePendingFibersToMemoized,
|
||||
addTransitionToLanesMap,
|
||||
getTransitionsForLanes,
|
||||
DefaultLane,
|
||||
} from './ReactFiberLane';
|
||||
import {
|
||||
DiscreteEventPriority,
|
||||
|
|
@ -922,13 +926,30 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
|
|||
);
|
||||
}
|
||||
}
|
||||
// The priority hasn't changed. We can reuse the existing task. Exit.
|
||||
return;
|
||||
|
||||
if (
|
||||
enableFrameEndScheduling &&
|
||||
supportsFrameAlignedTask &&
|
||||
newCallbackPriority === DefaultLane
|
||||
) {
|
||||
// Do nothing, we need to schedule a new rAF.
|
||||
} else {
|
||||
// The priority hasn't changed. We can reuse the existing task. Exit.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingCallbackNode != null) {
|
||||
if (existingCallbackNode !== null) {
|
||||
// Cancel the existing callback. We'll schedule a new one below.
|
||||
cancelCallback(existingCallbackNode);
|
||||
if (
|
||||
enableFrameEndScheduling &&
|
||||
supportsFrameAlignedTask &&
|
||||
existingCallbackPriority === DefaultLane
|
||||
) {
|
||||
cancelFrameAlignedTask(existingCallbackNode);
|
||||
} else {
|
||||
cancelCallback(existingCallbackNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule a new callback.
|
||||
|
|
@ -972,6 +993,23 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
|
|||
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
|
||||
}
|
||||
newCallbackNode = null;
|
||||
} else if (
|
||||
enableFrameEndScheduling &&
|
||||
supportsFrameAlignedTask &&
|
||||
newCallbackPriority === DefaultLane
|
||||
) {
|
||||
if (__DEV__ && ReactCurrentActQueue.current !== null) {
|
||||
// Inside `act`, use our internal `act` queue so that these get flushed
|
||||
// at the end of the current scope even when using the sync version
|
||||
// of `act`.
|
||||
ReactCurrentActQueue.current.push(
|
||||
performConcurrentWorkOnRoot.bind(null, root),
|
||||
);
|
||||
} else {
|
||||
newCallbackNode = scheduleFrameAlignedTask(
|
||||
performConcurrentWorkOnRoot.bind(null, root),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let schedulerPriorityLevel;
|
||||
switch (lanesToEventPriority(nextLanes)) {
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ type BaseFiberRootProperties = {
|
|||
// task that the root will work on.
|
||||
callbackNode: any,
|
||||
callbackPriority: Lane,
|
||||
frameAlignedNode?: number | null,
|
||||
eventTimes: LaneMap<number>,
|
||||
expirationTimes: LaneMap<number>,
|
||||
hiddenUpdates: LaneMap<Array<ConcurrentUpdate> | null>,
|
||||
|
|
|
|||
|
|
@ -157,12 +157,18 @@ describe('ReactBlockingMode', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
// Only the second update should have flushed synchronously
|
||||
expect(Scheduler).toHaveYielded(['B1']);
|
||||
expect(root).toMatchRenderedOutput('A0B1');
|
||||
|
||||
// Now flush the first update
|
||||
expect(Scheduler).toFlushAndYield(['A1']);
|
||||
expect(root).toMatchRenderedOutput('A1B1');
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toHaveYielded(['A1', 'B1']);
|
||||
expect(root).toMatchRenderedOutput('A1B1');
|
||||
} else {
|
||||
// Only the second update should have flushed synchronously
|
||||
expect(Scheduler).toHaveYielded(['B1']);
|
||||
expect(root).toMatchRenderedOutput('A0B1');
|
||||
|
||||
// Now flush the first update
|
||||
expect(Scheduler).toFlushAndYield(['A1']);
|
||||
expect(root).toMatchRenderedOutput('A1B1');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,9 +35,17 @@ describe('ReactClassSetStateCallback', () => {
|
|||
expect(Scheduler).toHaveYielded([0]);
|
||||
|
||||
await act(async () => {
|
||||
app.setState({step: 1}, () =>
|
||||
Scheduler.unstable_yieldValue('Callback 1'),
|
||||
);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
React.startTransition(() => {
|
||||
app.setState({step: 1}, () =>
|
||||
Scheduler.unstable_yieldValue('Callback 1'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
app.setState({step: 1}, () =>
|
||||
Scheduler.unstable_yieldValue('Callback 1'),
|
||||
);
|
||||
}
|
||||
ReactNoop.flushSync(() => {
|
||||
app.setState({step: 2}, () =>
|
||||
Scheduler.unstable_yieldValue('Callback 2'),
|
||||
|
|
|
|||
|
|
@ -339,7 +339,9 @@ describe('ReactExpiration', () => {
|
|||
// Before the update can finish, update again. Even though no time has
|
||||
// advanced, this update should be given a different expiration time than
|
||||
// the currently rendering one. So, C and D should render with 1, not 2.
|
||||
subscribers.forEach(s => s.setState({text: '2'}));
|
||||
React.startTransition(() => {
|
||||
subscribers.forEach(s => s.setState({text: '2'}));
|
||||
});
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'1 [C] [render]',
|
||||
'1 [D] [render]',
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ describe('ReactFiberHostContext', () => {
|
|||
prepareRendererToRender: function() {},
|
||||
resetRendererAfterRender: function() {},
|
||||
supportsMutation: true,
|
||||
shouldScheduleAnimationFrame: () => false,
|
||||
});
|
||||
|
||||
const container = Renderer.createContainer(
|
||||
|
|
|
|||
|
|
@ -54,15 +54,21 @@ describe('ReactFlushSync', () => {
|
|||
// The passive effect will schedule a sync update and a normal update.
|
||||
// They should commit in two separate batches. First the sync one.
|
||||
expect(() => {
|
||||
expect(Scheduler).toFlushUntilNextPaint(['1, 0']);
|
||||
expect(Scheduler).toFlushUntilNextPaint(
|
||||
gate(flags => flags.enableUnifiedSyncLane) ? ['1, 1'] : ['1, 0'],
|
||||
);
|
||||
}).toErrorDev('flushSync was called from inside a lifecycle method');
|
||||
|
||||
// The remaining update is not sync
|
||||
ReactNoop.flushSync();
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
|
||||
// Now flush it.
|
||||
expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toFlushUntilNextPaint([]);
|
||||
} else {
|
||||
// Now flush it.
|
||||
expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
|
||||
}
|
||||
});
|
||||
expect(root).toMatchRenderedOutput('1, 1');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -568,9 +568,13 @@ describe('ReactHooks', () => {
|
|||
});
|
||||
};
|
||||
|
||||
// Update at normal priority
|
||||
ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
|
||||
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
// Update at transition priority
|
||||
React.startTransition(() => update(n => n * 100));
|
||||
} else {
|
||||
// Update at normal priority
|
||||
ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
|
||||
}
|
||||
// The new state is eagerly computed.
|
||||
expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']);
|
||||
|
||||
|
|
|
|||
|
|
@ -815,7 +815,13 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
ReactNoop.discreteUpdates(() => {
|
||||
setRow(5);
|
||||
});
|
||||
setRow(20);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.startTransition(() => {
|
||||
setRow(20);
|
||||
});
|
||||
} else {
|
||||
setRow(20);
|
||||
}
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Up', 'Down']);
|
||||
expect(root).toMatchRenderedOutput(<span prop="Down" />);
|
||||
|
|
@ -955,11 +961,15 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
ReactNoop.flushSync(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Count: 4']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toHaveYielded(['Count: 4']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
|
||||
} else {
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 4']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1717,11 +1727,15 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
// As a result we, somewhat surprisingly, commit them in the opposite order.
|
||||
// This should be fine because any non-discrete set of work doesn't guarantee order
|
||||
// and easily could've happened slightly later too.
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Will set count to 1',
|
||||
'Count: 2',
|
||||
'Count: 1',
|
||||
]);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toHaveYielded(['Will set count to 1', 'Count: 1']);
|
||||
} else {
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Will set count to 1',
|
||||
'Count: 2',
|
||||
'Count: 1',
|
||||
]);
|
||||
}
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1910,21 +1910,37 @@ describe('ReactIncremental', () => {
|
|||
<ShowBoth />
|
||||
</Intl>,
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'ShowLocale {"locale":"sv"}',
|
||||
'ShowBoth {"locale":"sv"}',
|
||||
'Intl {}',
|
||||
'ShowLocale {"locale":"en"}',
|
||||
'Router {}',
|
||||
'Indirection {}',
|
||||
'ShowLocale {"locale":"en"}',
|
||||
'ShowRoute {"route":"/about"}',
|
||||
'ShowNeither {}',
|
||||
'Intl {}',
|
||||
'ShowBoth {"locale":"ru","route":"/about"}',
|
||||
'ShowBoth {"locale":"en","route":"/about"}',
|
||||
'ShowBoth {"locale":"en"}',
|
||||
]);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Intl {}',
|
||||
'ShowLocale {"locale":"en"}',
|
||||
'Router {}',
|
||||
'Indirection {}',
|
||||
'ShowLocale {"locale":"en"}',
|
||||
'ShowRoute {"route":"/about"}',
|
||||
'ShowNeither {}',
|
||||
'Intl {}',
|
||||
'ShowBoth {"locale":"ru","route":"/about"}',
|
||||
'ShowBoth {"locale":"en","route":"/about"}',
|
||||
'ShowBoth {"locale":"en"}',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'ShowLocale {"locale":"sv"}',
|
||||
'ShowBoth {"locale":"sv"}',
|
||||
'Intl {}',
|
||||
'ShowLocale {"locale":"en"}',
|
||||
'Router {}',
|
||||
'Indirection {}',
|
||||
'ShowLocale {"locale":"en"}',
|
||||
'ShowRoute {"route":"/about"}',
|
||||
'ShowNeither {}',
|
||||
'Intl {}',
|
||||
'ShowBoth {"locale":"ru","route":"/about"}',
|
||||
'ShowBoth {"locale":"en","route":"/about"}',
|
||||
'ShowBoth {"locale":"en"}',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not leak own context into context provider', () => {
|
||||
|
|
@ -2758,7 +2774,11 @@ describe('ReactIncremental', () => {
|
|||
// Interrupt at same priority
|
||||
ReactNoop.render(<Parent step={2} />);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 2', 'Child: 2']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not interrupt for update at lower priority', () => {
|
||||
|
|
@ -2785,7 +2805,11 @@ describe('ReactIncremental', () => {
|
|||
ReactNoop.expire(2000);
|
||||
ReactNoop.render(<Parent step={2} />);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 2', 'Child: 2']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
|
||||
}
|
||||
});
|
||||
|
||||
it('does interrupt for update at higher priority', () => {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ describe('ReactIncrementalUpdates', () => {
|
|||
state = {};
|
||||
componentDidMount() {
|
||||
Scheduler.unstable_yieldValue('commit');
|
||||
ReactNoop.deferredUpdates(() => {
|
||||
React.startTransition(() => {
|
||||
// Has low priority
|
||||
this.setState({b: 'b'});
|
||||
this.setState({c: 'c'});
|
||||
|
|
@ -111,13 +111,13 @@ describe('ReactIncrementalUpdates', () => {
|
|||
expect(Scheduler).toFlushAndYield(['render', 'componentDidMount']);
|
||||
|
||||
ReactNoop.flushSync(() => {
|
||||
ReactNoop.deferredUpdates(() => {
|
||||
React.startTransition(() => {
|
||||
instance.setState({x: 'x'});
|
||||
instance.setState({y: 'y'});
|
||||
});
|
||||
instance.setState({a: 'a'});
|
||||
instance.setState({b: 'b'});
|
||||
ReactNoop.deferredUpdates(() => {
|
||||
React.startTransition(() => {
|
||||
instance.updater.enqueueReplaceState(instance, {c: 'c'});
|
||||
instance.setState({d: 'd'});
|
||||
});
|
||||
|
|
@ -162,7 +162,11 @@ describe('ReactIncrementalUpdates', () => {
|
|||
}
|
||||
|
||||
// Schedule some async updates
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
if (
|
||||
gate(
|
||||
flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane,
|
||||
)
|
||||
) {
|
||||
React.startTransition(() => {
|
||||
instance.setState(createUpdate('a'));
|
||||
instance.setState(createUpdate('b'));
|
||||
|
|
@ -179,23 +183,37 @@ describe('ReactIncrementalUpdates', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('')]);
|
||||
|
||||
// Schedule some more updates at different priorities
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
instance.setState(createUpdate('f'));
|
||||
});
|
||||
React.startTransition(() => {
|
||||
instance.setState(createUpdate('g'));
|
||||
});
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
instance.setState(createUpdate('f'));
|
||||
});
|
||||
React.startTransition(() => {
|
||||
instance.setState(createUpdate('g'));
|
||||
});
|
||||
|
||||
// The sync updates should have flushed, but not the async ones
|
||||
// The sync updates should have flushed, but not the async ones.
|
||||
if (
|
||||
gate(
|
||||
flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane,
|
||||
)
|
||||
) {
|
||||
expect(Scheduler).toHaveYielded(['d', 'e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('def')]);
|
||||
} else {
|
||||
// Update d was dropped and replaced by e.
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('ef')]);
|
||||
}
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
if (
|
||||
gate(
|
||||
flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
|
||||
)
|
||||
) {
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Since 'g' is in a transition, we'll process 'd' separately first.
|
||||
// That causes us to process 'd' with 'e' and 'f' rebased.
|
||||
|
|
@ -211,25 +229,19 @@ describe('ReactIncrementalUpdates', () => {
|
|||
'f',
|
||||
'g',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
|
||||
} else {
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
instance.setState(createUpdate('f'));
|
||||
});
|
||||
instance.setState(createUpdate('g'));
|
||||
|
||||
// The sync updates should have flushed, but not the async ones
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('ef')]);
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Then we'll re-process everything for 'g'.
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
]);
|
||||
}
|
||||
expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
|
||||
});
|
||||
|
||||
it('can abort an update, schedule a replaceState, and resume', () => {
|
||||
|
|
@ -261,7 +273,11 @@ describe('ReactIncrementalUpdates', () => {
|
|||
}
|
||||
|
||||
// Schedule some async updates
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
if (
|
||||
gate(
|
||||
flags => flags.enableSyncDefaultUpdates || flags.enableUnifiedSyncLane,
|
||||
)
|
||||
) {
|
||||
React.startTransition(() => {
|
||||
instance.setState(createUpdate('a'));
|
||||
instance.setState(createUpdate('b'));
|
||||
|
|
@ -278,26 +294,39 @@ describe('ReactIncrementalUpdates', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('')]);
|
||||
|
||||
// Schedule some more updates at different priorities
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
instance.setState(createUpdate('d'));
|
||||
instance.setState(createUpdate('d'));
|
||||
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
// No longer a public API, but we can test that it works internally by
|
||||
// reaching into the updater.
|
||||
instance.updater.enqueueReplaceState(instance, createUpdate('f'));
|
||||
});
|
||||
React.startTransition(() => {
|
||||
instance.setState(createUpdate('g'));
|
||||
});
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
// No longer a public API, but we can test that it works internally by
|
||||
// reaching into the updater.
|
||||
instance.updater.enqueueReplaceState(instance, createUpdate('f'));
|
||||
});
|
||||
React.startTransition(() => {
|
||||
instance.setState(createUpdate('g'));
|
||||
});
|
||||
|
||||
// The sync updates should have flushed, but not the async ones.
|
||||
// The sync updates should have flushed, but not the async ones.
|
||||
if (
|
||||
gate(
|
||||
flags => flags.enableSyncDefaultUpdates && flags.enableUnifiedSyncLane,
|
||||
)
|
||||
) {
|
||||
expect(Scheduler).toHaveYielded(['d', 'e', 'f']);
|
||||
} else {
|
||||
// Update d was dropped and replaced by e.
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('f')]);
|
||||
}
|
||||
expect(ReactNoop.getChildren()).toEqual([span('f')]);
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
if (
|
||||
gate(
|
||||
flags => flags.enableSyncDefaultUpdates && !flags.enableUnifiedSyncLane,
|
||||
)
|
||||
) {
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Since 'g' is in a transition, we'll process 'd' separately first.
|
||||
// That causes us to process 'd' with 'e' and 'f' rebased.
|
||||
|
|
@ -313,28 +342,19 @@ describe('ReactIncrementalUpdates', () => {
|
|||
'f',
|
||||
'g',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('fg')]);
|
||||
} else {
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
// No longer a public API, but we can test that it works internally by
|
||||
// reaching into the updater.
|
||||
instance.updater.enqueueReplaceState(instance, createUpdate('f'));
|
||||
});
|
||||
instance.setState(createUpdate('g'));
|
||||
|
||||
// The sync updates should have flushed, but not the async ones. Update d
|
||||
// was dropped and replaced by e.
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('f')]);
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('fg')]);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Then we'll re-process everything for 'g'.
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
]);
|
||||
}
|
||||
expect(ReactNoop.getChildren()).toEqual([span('fg')]);
|
||||
});
|
||||
|
||||
it('passes accumulation of previous updates to replaceState updater function', () => {
|
||||
|
|
@ -688,21 +708,29 @@ describe('ReactIncrementalUpdates', () => {
|
|||
pushToLog('B'),
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// A and B are pending. B is higher priority, so we'll render that first.
|
||||
'Committed: B',
|
||||
// Because A comes first in the queue, we're now in rebase mode. B must
|
||||
// be rebased on top of A. Also, in a layout effect, we received two new
|
||||
// updates: C and D. C is user-blocking and D is synchronous.
|
||||
//
|
||||
// First render the synchronous update. What we're testing here is that
|
||||
// B *is not dropped* even though it has lower than sync priority. That's
|
||||
// because we already committed it. However, this render should not
|
||||
// include C, because that update wasn't already committed.
|
||||
'Committed: BD',
|
||||
'Committed: BCD',
|
||||
'Committed: ABCD',
|
||||
]);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Committed: B',
|
||||
'Committed: BCD',
|
||||
'Committed: ABCD',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// A and B are pending. B is higher priority, so we'll render that first.
|
||||
'Committed: B',
|
||||
// Because A comes first in the queue, we're now in rebase mode. B must
|
||||
// be rebased on top of A. Also, in a layout effect, we received two new
|
||||
// updates: C and D. C is user-blocking and D is synchronous.
|
||||
//
|
||||
// First render the synchronous update. What we're testing here is that
|
||||
// B *is not dropped* even though it has lower than sync priority. That's
|
||||
// because we already committed it. However, this render should not
|
||||
// include C, because that update wasn't already committed.
|
||||
'Committed: BD',
|
||||
'Committed: BCD',
|
||||
'Committed: ABCD',
|
||||
]);
|
||||
}
|
||||
expect(root).toMatchRenderedOutput('ABCD');
|
||||
});
|
||||
|
||||
|
|
@ -748,21 +776,29 @@ describe('ReactIncrementalUpdates', () => {
|
|||
pushToLog('B'),
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// A and B are pending. B is higher priority, so we'll render that first.
|
||||
'Committed: B',
|
||||
// Because A comes first in the queue, we're now in rebase mode. B must
|
||||
// be rebased on top of A. Also, in a layout effect, we received two new
|
||||
// updates: C and D. C is user-blocking and D is synchronous.
|
||||
//
|
||||
// First render the synchronous update. What we're testing here is that
|
||||
// B *is not dropped* even though it has lower than sync priority. That's
|
||||
// because we already committed it. However, this render should not
|
||||
// include C, because that update wasn't already committed.
|
||||
'Committed: BD',
|
||||
'Committed: BCD',
|
||||
'Committed: ABCD',
|
||||
]);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Committed: B',
|
||||
'Committed: BCD',
|
||||
'Committed: ABCD',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// A and B are pending. B is higher priority, so we'll render that first.
|
||||
'Committed: B',
|
||||
// Because A comes first in the queue, we're now in rebase mode. B must
|
||||
// be rebased on top of A. Also, in a layout effect, we received two new
|
||||
// updates: C and D. C is user-blocking and D is synchronous.
|
||||
//
|
||||
// First render the synchronous update. What we're testing here is that
|
||||
// B *is not dropped* even though it has lower than sync priority. That's
|
||||
// because we already committed it. However, this render should not
|
||||
// include C, because that update wasn't already committed.
|
||||
'Committed: BD',
|
||||
'Committed: BCD',
|
||||
'Committed: ABCD',
|
||||
]);
|
||||
}
|
||||
expect(root).toMatchRenderedOutput('ABCD');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -690,8 +690,15 @@ describe('ReactOffscreen', () => {
|
|||
);
|
||||
|
||||
// Before the inner update can finish, we receive another pair of updates.
|
||||
setOuter(2);
|
||||
setInner(2);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
React.startTransition(() => {
|
||||
setOuter(2);
|
||||
setInner(2);
|
||||
});
|
||||
} else {
|
||||
setOuter(2);
|
||||
setInner(2);
|
||||
}
|
||||
|
||||
// Also, before either of these new updates are processed, the hidden
|
||||
// tree is revealed at high priority.
|
||||
|
|
|
|||
|
|
@ -381,7 +381,9 @@ describe('ReactOffscreen', () => {
|
|||
expect(root).toMatchRenderedOutput(<span hidden={true}>A0</span>);
|
||||
|
||||
await act(async () => {
|
||||
setStep(1);
|
||||
React.startTransition(() => {
|
||||
setStep(1);
|
||||
});
|
||||
ReactNoop.flushSync(() => {
|
||||
setText('B');
|
||||
});
|
||||
|
|
@ -513,8 +515,10 @@ describe('ReactOffscreen', () => {
|
|||
|
||||
// Before the tree commits, schedule a concurrent event. The inner update
|
||||
// is to a tree that's just about to be hidden.
|
||||
setOuter(2);
|
||||
setInner(2);
|
||||
startTransition(() => {
|
||||
setOuter(2);
|
||||
setInner(2);
|
||||
});
|
||||
|
||||
// Commit the previous render.
|
||||
jest.runAllTimers();
|
||||
|
|
|
|||
|
|
@ -934,16 +934,30 @@ describe('ReactTransition', () => {
|
|||
updateNormalPri();
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// Finish transition update.
|
||||
'Normal pri: 0',
|
||||
'Commit',
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// Interrupt transition.
|
||||
'Transition pri: 0',
|
||||
'Normal pri: 1',
|
||||
'Commit',
|
||||
|
||||
// Normal pri update.
|
||||
'Transition pri: 1',
|
||||
'Normal pri: 1',
|
||||
'Commit',
|
||||
]);
|
||||
// Normal pri update.
|
||||
'Transition pri: 1',
|
||||
'Normal pri: 1',
|
||||
'Commit',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// Finish transition update.
|
||||
'Normal pri: 0',
|
||||
'Commit',
|
||||
|
||||
// Normal pri update.
|
||||
'Transition pri: 1',
|
||||
'Normal pri: 1',
|
||||
'Commit',
|
||||
]);
|
||||
}
|
||||
|
||||
expect(root).toMatchRenderedOutput('Transition pri: 1, Normal pri: 1');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1558,8 +1558,15 @@ describe('useMutableSource', () => {
|
|||
expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']);
|
||||
// Mutate in an event. This schedules a subscription update on a, which
|
||||
// already mounted, but not b, which hasn't subscribed yet.
|
||||
mutateA('a1');
|
||||
mutateB('b1');
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
React.startTransition(() => {
|
||||
mutateA('a1');
|
||||
mutateB('b1');
|
||||
});
|
||||
} else {
|
||||
mutateA('a1');
|
||||
mutateB('b1');
|
||||
}
|
||||
|
||||
// Mutate again at lower priority. This will schedule another subscription
|
||||
// update on a, but not b. When b mounts and subscriptions, the value it
|
||||
|
|
|
|||
|
|
@ -78,6 +78,14 @@ export const resetRendererAfterRender = $$$hostConfig.resetRendererAfterRender;
|
|||
export const supportsMicrotasks = $$$hostConfig.supportsMicrotasks;
|
||||
export const scheduleMicrotask = $$$hostConfig.scheduleMicrotask;
|
||||
|
||||
// -------------------
|
||||
// Animation Frame
|
||||
// (optional)
|
||||
// -------------------
|
||||
export const supportsFrameAlignedTask = $$$hostConfig.supportsFrameAlignedTask;
|
||||
export const scheduleFrameAlignedTask = $$$hostConfig.scheduleFrameAlignedTask;
|
||||
export const cancelFrameAlignedTask = $$$hostConfig.cancelFrameAlignedTask;
|
||||
|
||||
// -------------------
|
||||
// Test selectors
|
||||
// (optional)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ export const enableLegacyCache = __EXPERIMENTAL__;
|
|||
export const enableCacheElement = __EXPERIMENTAL__;
|
||||
export const enableFetchInstrumentation = true;
|
||||
|
||||
export const enableFrameEndScheduling = __EXPERIMENTAL__;
|
||||
|
||||
export const enableTransitionTracing = false;
|
||||
|
||||
// No known bugs, but needs performance testing
|
||||
|
|
@ -151,6 +153,8 @@ export const enableUseRefAccessWarning = false;
|
|||
// Enables time slicing for updates that aren't wrapped in startTransition.
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
export const enableUnifiedSyncLane = __EXPERIMENTAL__;
|
||||
|
||||
// Adds an opt-in to time slicing for updates that aren't wrapped in
|
||||
// startTransition. Only relevant when enableSyncDefaultUpdates is disabled.
|
||||
export const allowConcurrentByDefault = false;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export const enableCache = false;
|
|||
export const enableLegacyCache = false;
|
||||
export const enableCacheElement = true;
|
||||
export const enableFetchInstrumentation = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const debugRenderPhaseSideEffectsForStrictMode = true;
|
||||
export const disableJavaScriptURLs = false;
|
||||
|
|
@ -72,6 +73,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
|
|||
export const enableLazyContextPropagation = false;
|
||||
export const enableLegacyHidden = true;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
export const enableUnifiedSyncLane = false;
|
||||
export const allowConcurrentByDefault = true;
|
||||
export const enableCustomElementPropertySupport = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const enableCache = false;
|
|||
export const enableLegacyCache = false;
|
||||
export const enableCacheElement = false;
|
||||
export const enableFetchInstrumentation = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
|
|
@ -63,6 +64,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
|
|||
export const enableLazyContextPropagation = false;
|
||||
export const enableLegacyHidden = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
export const enableUnifiedSyncLane = false;
|
||||
export const allowConcurrentByDefault = false;
|
||||
export const enableCustomElementPropertySupport = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const enableCache = true;
|
|||
export const enableLegacyCache = __EXPERIMENTAL__;
|
||||
export const enableCacheElement = __EXPERIMENTAL__;
|
||||
export const enableFetchInstrumentation = true;
|
||||
export const enableFrameEndScheduling = __EXPERIMENTAL__;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
|
|
@ -63,6 +64,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
|
|||
export const enableLazyContextPropagation = false;
|
||||
export const enableLegacyHidden = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
export const enableUnifiedSyncLane = __EXPERIMENTAL__;
|
||||
export const allowConcurrentByDefault = false;
|
||||
export const enableCustomElementPropertySupport = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const enableCache = true;
|
|||
export const enableLegacyCache = false;
|
||||
export const enableCacheElement = true;
|
||||
export const enableFetchInstrumentation = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
|
|
@ -62,6 +63,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
|
|||
export const enableLazyContextPropagation = false;
|
||||
export const enableLegacyHidden = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
export const enableUnifiedSyncLane = false;
|
||||
export const allowConcurrentByDefault = true;
|
||||
|
||||
export const consoleManagedByDevToolsDuringStrictMode = false;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const enableCache = true;
|
|||
export const enableLegacyCache = true;
|
||||
export const enableCacheElement = true;
|
||||
export const enableFetchInstrumentation = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
|
|
@ -63,6 +64,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
|
|||
export const enableLazyContextPropagation = false;
|
||||
export const enableLegacyHidden = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
export const enableUnifiedSyncLane = false;
|
||||
export const allowConcurrentByDefault = true;
|
||||
export const enableCustomElementPropertySupport = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const enableCache = __EXPERIMENTAL__;
|
|||
export const enableLegacyCache = __EXPERIMENTAL__;
|
||||
export const enableCacheElement = __EXPERIMENTAL__;
|
||||
export const enableFetchInstrumentation = __EXPERIMENTAL__;
|
||||
export const enableFrameEndScheduling = __EXPERIMENTAL__;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
|
|
@ -63,6 +64,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
|
|||
export const enableLazyContextPropagation = false;
|
||||
export const enableLegacyHidden = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
export const enableUnifiedSyncLane = __EXPERIMENTAL__;
|
||||
export const allowConcurrentByDefault = false;
|
||||
export const enableCustomElementPropertySupport = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const enableCache = true;
|
|||
export const enableLegacyCache = true;
|
||||
export const enableCacheElement = true;
|
||||
export const enableFetchInstrumentation = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const disableJavaScriptURLs = true;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
|
|
@ -63,6 +64,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
|
|||
export const enableLazyContextPropagation = false;
|
||||
export const enableLegacyHidden = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
export const enableUnifiedSyncLane = __EXPERIMENTAL__;
|
||||
export const allowConcurrentByDefault = true;
|
||||
export const enableCustomElementPropertySupport = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
|
|||
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
|
||||
export const enableLazyContextPropagation = __VARIANT__;
|
||||
export const enableSyncDefaultUpdates = __VARIANT__;
|
||||
export const enableUnifiedSyncLane = __VARIANT__;
|
||||
export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
|
||||
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
|
||||
export const enableClientRenderFallbackOnTextMismatch = __VARIANT__;
|
||||
export const enableTransitionTracing = __VARIANT__;
|
||||
export const enableFrameEndScheduling = __VARIANT__;
|
||||
// Enable this flag to help with concurrent mode debugging.
|
||||
// It logs information to the console about React scheduling, rendering, and commit phases.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -31,8 +31,10 @@ export const {
|
|||
disableSchedulerTimeoutInWorkLoop,
|
||||
enableLazyContextPropagation,
|
||||
enableSyncDefaultUpdates,
|
||||
enableUnifiedSyncLane,
|
||||
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
|
||||
enableClientRenderFallbackOnTextMismatch,
|
||||
enableFrameEndScheduling,
|
||||
enableTransitionTracing,
|
||||
} = dynamicFeatureFlags;
|
||||
|
||||
|
|
|
|||
|
|
@ -454,7 +454,13 @@ describe('useSubscription', () => {
|
|||
observableA.next('a-2');
|
||||
|
||||
// Update again
|
||||
renderer.update(<Parent observed={observableA} />);
|
||||
if (gate(flags => flags.enableUnifiedSyncLane)) {
|
||||
React.startTransition(() => {
|
||||
renderer.update(<Parent observed={observableA} />);
|
||||
});
|
||||
} else {
|
||||
renderer.update(<Parent observed={observableA} />);
|
||||
}
|
||||
|
||||
// Flush everything and ensure that the correct subscribable is used
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
|
|
|
|||
|
|
@ -35,4 +35,28 @@ if (typeof window !== 'undefined') {
|
|||
global.cancelIdleCallback = function(callbackID) {
|
||||
clearTimeout(callbackID);
|
||||
};
|
||||
|
||||
// We need to mock rAF because Jest 26 does not flush rAF.
|
||||
// Once we upgrade to Jest 27+, rAF is flushed every 16ms.
|
||||
global.requestAnimationFrameQueue = null;
|
||||
global.requestAnimationFrame = function(callback) {
|
||||
if (global.requestAnimationFrameQueue == null) {
|
||||
global.requestAnimationFrameQueue = [];
|
||||
}
|
||||
global.requestAnimationFrameQueue.push(callback);
|
||||
return global.requestAnimationFrameQueue.length - 1;
|
||||
};
|
||||
|
||||
global.cancelAnimationFrame = function(id) {
|
||||
if (global.requestAnimationFrameQueue != null) {
|
||||
global.requestAnimationFrameQueue.splice(id, 1);
|
||||
}
|
||||
};
|
||||
|
||||
global.flushRequestAnimationFrameQueue = function() {
|
||||
if (global.requestAnimationFrameQueue != null) {
|
||||
global.requestAnimationFrameQueue.forEach(callback => callback());
|
||||
global.requestAnimationFrameQueue = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
|
|||
throw error;
|
||||
}
|
||||
});
|
||||
env.beforeEach(() => {
|
||||
// TODO: warn if this has not flushed.
|
||||
global.requestAnimationFrameQueue = null;
|
||||
});
|
||||
|
||||
// TODO: Consider consolidating this with `yieldValue`. In both cases, tests
|
||||
// should not be allowed to exit without asserting on the entire log.
|
||||
|
|
|
|||
Loading…
Reference in New Issue