Compare commits
1 Commits
eslint-plu
...
ty-rh-defa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e02f5b4bd |
@@ -33,6 +33,7 @@ import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostCo
|
||||
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 hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
|
||||
@@ -623,6 +624,11 @@ const localRequestAnimationFrame =
|
||||
typeof requestAnimationFrame === 'function'
|
||||
? requestAnimationFrame
|
||||
: scheduleTimeout;
|
||||
const localCancelAnimationFrame =
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.cancelAnimationFrame === 'function'
|
||||
? window.cancelAnimationFrame
|
||||
: cancelTimeout;
|
||||
|
||||
export function getInstanceFromNode(node: HTMLElement): null | Object {
|
||||
return getClosestInstanceFromNode(node) || null;
|
||||
@@ -668,6 +674,68 @@ function handleErrorInNextTick(error: any) {
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// Animation Frame
|
||||
// -------------------
|
||||
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;
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// Mutation
|
||||
// -------------------
|
||||
|
||||
@@ -472,6 +472,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)', async () => {
|
||||
|
||||
@@ -408,6 +408,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" />);
|
||||
|
||||
@@ -415,7 +418,12 @@ describe('ReactDOMRoot', () => {
|
||||
expect(container.textContent).toEqual('a');
|
||||
|
||||
await waitFor(['b']);
|
||||
if (gate(flags => flags.allowConcurrentByDefault)) {
|
||||
if (
|
||||
gate(
|
||||
flags =>
|
||||
flags.allowConcurrentByDefault && !flags.enableFrameEndScheduling,
|
||||
)
|
||||
) {
|
||||
expect(container.textContent).toEqual('a');
|
||||
} else {
|
||||
expect(container.textContent).toEqual('b');
|
||||
|
||||
@@ -1404,7 +1404,11 @@ 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.",
|
||||
);
|
||||
|
||||
|
||||
@@ -142,24 +142,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 () => {
|
||||
@@ -187,24 +187,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 () => {
|
||||
@@ -226,14 +226,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 () => {
|
||||
@@ -262,17 +262,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 () => {
|
||||
@@ -342,12 +342,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 () => {
|
||||
@@ -367,12 +367,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 () => {
|
||||
@@ -396,24 +396,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 () => {
|
||||
@@ -447,26 +447,26 @@ describe('useId', () => {
|
||||
const root = ReactDOMClient.hydrateRoot(container, <App />);
|
||||
await waitForPaint([]);
|
||||
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);
|
||||
@@ -476,26 +476,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);
|
||||
});
|
||||
@@ -528,23 +528,23 @@ describe('useId', () => {
|
||||
const root = ReactDOMClient.hydrateRoot(container, <App />);
|
||||
await waitForPaint([]);
|
||||
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);
|
||||
@@ -554,23 +554,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);
|
||||
});
|
||||
@@ -604,37 +604,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
|
||||
@@ -668,36 +668,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>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -499,6 +499,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
})
|
||||
: setTimeout,
|
||||
|
||||
supportsFrameAlignedTask: false,
|
||||
scheduleFrameAlignedTask: undefined,
|
||||
cancelFrameAlignedTask: undefined,
|
||||
|
||||
prepareForCommit(): null | Object {
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -198,6 +198,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`.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
allowConcurrentByDefault,
|
||||
enableTransitionTracing,
|
||||
enableUnifiedSyncLane,
|
||||
enableFrameEndScheduling,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
|
||||
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
|
||||
@@ -498,7 +499,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 =
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
enableUpdaterTracking,
|
||||
enableCache,
|
||||
enableTransitionTracing,
|
||||
enableFrameEndScheduling,
|
||||
useModernStrictMode,
|
||||
disableLegacyContext,
|
||||
alwaysThrottleRetries,
|
||||
|
||||
@@ -86,6 +86,7 @@ describe('ReactFiberHostContext', () => {
|
||||
return null;
|
||||
},
|
||||
supportsMutation: true,
|
||||
shouldScheduleAnimationFrame: () => false,
|
||||
});
|
||||
|
||||
const container = Renderer.createContainer(
|
||||
|
||||
@@ -84,6 +84,14 @@ export const NotPendingTransition = $$$config.NotPendingTransition;
|
||||
export const supportsMicrotasks = $$$config.supportsMicrotasks;
|
||||
export const scheduleMicrotask = $$$config.scheduleMicrotask;
|
||||
|
||||
// -------------------
|
||||
// Animation Frame
|
||||
// (optional)
|
||||
// -------------------
|
||||
export const supportsFrameAlignedTask = $$$config.supportsFrameAlignedTask;
|
||||
export const scheduleFrameAlignedTask = $$$config.scheduleFrameAlignedTask;
|
||||
export const cancelFrameAlignedTask = $$$config.cancelFrameAlignedTask;
|
||||
|
||||
// -------------------
|
||||
// Test selectors
|
||||
// (optional)
|
||||
|
||||
@@ -89,6 +89,7 @@ export const enableBinaryFlight = __EXPERIMENTAL__;
|
||||
export const enableTaint = __EXPERIMENTAL__;
|
||||
|
||||
export const enablePostpone = __EXPERIMENTAL__;
|
||||
export const enableFrameEndScheduling = __EXPERIMENTAL__;
|
||||
|
||||
export const enableTransitionTracing = false;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export const enableFormActions = true; // Doesn't affect Native
|
||||
export const enableBinaryFlight = true;
|
||||
export const enableTaint = true;
|
||||
export const enablePostpone = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const debugRenderPhaseSideEffectsForStrictMode = true;
|
||||
export const disableJavaScriptURLs = false;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const enableFormActions = true; // Doesn't affect Native
|
||||
export const enableBinaryFlight = true;
|
||||
export const enableTaint = true;
|
||||
export const enablePostpone = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const enableFormActions = true; // Doesn't affect Test Renderer
|
||||
export const enableBinaryFlight = true;
|
||||
export const enableTaint = true;
|
||||
export const enablePostpone = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const enableFormActions = true; // Doesn't affect Test Renderer
|
||||
export const enableBinaryFlight = true;
|
||||
export const enableTaint = true;
|
||||
export const enablePostpone = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
export const disableInputAttributeSyncing = false;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const enableFormActions = true; // Doesn't affect Test Renderer
|
||||
export const enableBinaryFlight = true;
|
||||
export const enableTaint = true;
|
||||
export const enablePostpone = false;
|
||||
export const enableFrameEndScheduling = false;
|
||||
export const enableSchedulerDebugging = false;
|
||||
export const disableJavaScriptURLs = false;
|
||||
export const disableCommentsAsDOMContainers = true;
|
||||
|
||||
@@ -29,6 +29,7 @@ export const enableAsyncActions = __VARIANT__;
|
||||
export const alwaysThrottleRetries = __VARIANT__;
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
|
||||
export const enableUseDeferredValueInitialArg = __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.
|
||||
|
||||
@@ -24,6 +24,7 @@ export const {
|
||||
enableUseRefAccessWarning,
|
||||
enableLazyContextPropagation,
|
||||
enableUnifiedSyncLane,
|
||||
enableFrameEndScheduling,
|
||||
enableTransitionTracing,
|
||||
enableCustomElementPropertySupport,
|
||||
enableDeferRootSchedulingToMicrotask,
|
||||
|
||||
@@ -38,4 +38,28 @@ if (typeof window !== 'undefined') {
|
||||
} else {
|
||||
global.AbortController =
|
||||
require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController;
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
|
||||
// loop must always fail the test!
|
||||
beforeEach(() => {
|
||||
global.infiniteLoopError = null;
|
||||
// TODO: warn if this has not flushed.
|
||||
global.requestAnimationFrameQueue = null;
|
||||
});
|
||||
afterEach(() => {
|
||||
const error = global.infiniteLoopError;
|
||||
|
||||
Reference in New Issue
Block a user