Skip to content

Commit

Permalink
Add frame-end scheduling (V3)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickhanlonii committed Jun 9, 2022
1 parent 8186b19 commit 0a0565e
Show file tree
Hide file tree
Showing 41 changed files with 843 additions and 80 deletions.
10 changes: 10 additions & 0 deletions packages/jest-react/src/internalAct.js
Expand Up @@ -103,6 +103,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) {
Expand All @@ -126,6 +131,11 @@ function flushActWork(resolve, reject) {
enqueueTask(() => {
try {
const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();

// Flush scheduled rAF.
if (global.flushRequestAnimationFrameQueue) {
global.flushRequestAnimationFrameQueue();
}
if (didFlushWork) {
flushActWork(resolve, reject);
} else {
Expand Down
358 changes: 357 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
Expand Up @@ -160,7 +160,7 @@ describe('ReactDOMFiberAsync', () => {

handleChange = e => {
const nextValue = e.target.value;
requestIdleCallback(() => {
React.startTransition(() => {
this.setState({
asyncValue: nextValue,
});
Expand Down Expand Up @@ -545,6 +545,362 @@ 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', () => {
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');
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('Default update followed by an unknown update is batched, scheduled in a rAF', () => {
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 = 'test';
setState(1);

// We should not schedule a rAF for default updates only.
expect(global.requestAnimationFrameQueue).toBe(null);

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', () => {
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 = 'test';
setState(1);

// We should not schedule a rAF for default updates only.
expect(global.requestAnimationFrameQueue).toBe(null);

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', () => {
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, {
unstable_concurrentUpdatesByDefault: true,
});
act(() => {
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', () => {
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, {
unstable_concurrentUpdatesByDefault: true,
});
act(() => {
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', () => {
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, {
unstable_concurrentUpdatesByDefault: true,
});
act(() => {
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, {
unstable_concurrentUpdatesByDefault: true,
});
act(() => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);

window.event = undefined;
setState(1);

// Dispatch a click event on the button.
const firstEvent = document.createEvent('Event');
firstEvent.initEvent('click', true, true);
counterRef.current.dispatchEvent(firstEvent);

await null;

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
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, {
unstable_concurrentUpdatesByDefault: true,
});
act(() => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);

window.event = undefined;
setState(1);
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 1']);

setState(2);
setThrowing(true);

global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2', 'suspending']);
expect(counterRef.current.textContent).toBe('Count: 1');

unsuspend();
setThrowing(false);

// Should not be scheduled in a rAF.
window.event = 'test';
setState(2);

// TODO: This should not yield
// global.flushRequestAnimationFrameQueue();
// expect(Scheduler).toHaveYielded([]);

expect(Scheduler).toFlushAndYield(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
});

it('regression test: does not drop passive effects across roots (#17066)', () => {
Expand Down

0 comments on commit 0a0565e

Please sign in to comment.