/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactFeatureFlags;
let ReactDOM;
let ReactDOMClient;
let Scheduler;
let mockDevToolsHook;
let allSchedulerTags;
let allSchedulerTypes;
let onCommitRootShouldYield;
let act;
let waitFor;
let waitForAll;
let assertLog;
describe('updaters', () => {
beforeEach(() => {
jest.resetModules();
allSchedulerTags = [];
allSchedulerTypes = [];
onCommitRootShouldYield = true;
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableUpdaterTracking = true;
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
mockDevToolsHook = {
injectInternals: jest.fn(() => {}),
isDevToolsPresent: true,
onCommitRoot: jest.fn(fiberRoot => {
if (onCommitRootShouldYield) {
Scheduler.log('onCommitRoot');
}
const schedulerTags = [];
const schedulerTypes = [];
fiberRoot.memoizedUpdaters.forEach(fiber => {
schedulerTags.push(fiber.tag);
schedulerTypes.push(fiber.elementType);
});
allSchedulerTags.push(schedulerTags);
allSchedulerTypes.push(schedulerTypes);
}),
onCommitUnmount: jest.fn(() => {}),
onPostCommitRoot: jest.fn(() => {}),
onScheduleRoot: jest.fn(() => {}),
// Profiling APIs
markCommitStarted: jest.fn(() => {}),
markCommitStopped: jest.fn(() => {}),
markComponentRenderStarted: jest.fn(() => {}),
markComponentRenderStopped: jest.fn(() => {}),
markComponentPassiveEffectMountStarted: jest.fn(() => {}),
markComponentPassiveEffectMountStopped: jest.fn(() => {}),
markComponentPassiveEffectUnmountStarted: jest.fn(() => {}),
markComponentPassiveEffectUnmountStopped: jest.fn(() => {}),
markComponentLayoutEffectMountStarted: jest.fn(() => {}),
markComponentLayoutEffectMountStopped: jest.fn(() => {}),
markComponentLayoutEffectUnmountStarted: jest.fn(() => {}),
markComponentLayoutEffectUnmountStopped: jest.fn(() => {}),
markComponentErrored: jest.fn(() => {}),
markComponentSuspended: jest.fn(() => {}),
markLayoutEffectsStarted: jest.fn(() => {}),
markLayoutEffectsStopped: jest.fn(() => {}),
markPassiveEffectsStarted: jest.fn(() => {}),
markPassiveEffectsStopped: jest.fn(() => {}),
markRenderStarted: jest.fn(() => {}),
markRenderYielded: jest.fn(() => {}),
markRenderStopped: jest.fn(() => {}),
markRenderScheduled: jest.fn(() => {}),
markForceUpdateScheduled: jest.fn(() => {}),
markStateUpdateScheduled: jest.fn(() => {}),
};
jest.mock(
'react-reconciler/src/ReactFiberDevToolsHook',
() => mockDevToolsHook,
);
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
});
it('should report the (host) root as the scheduler for root-level render', async () => {
const {HostRoot} = require('react-reconciler/src/ReactWorkTags');
const Parent = () => <Child />;
const Child = () => null;
const container = document.createElement('div');
await act(() => {
ReactDOM.render(<Parent />, container);
});
expect(allSchedulerTags).toEqual([[HostRoot]]);
await act(() => {
ReactDOM.render(<Parent />, container);
});
expect(allSchedulerTags).toEqual([[HostRoot], [HostRoot]]);
});
it('should report a function component as the scheduler for a hooks update', async () => {
let scheduleForA = null;
let scheduleForB = null;
const Parent = () => (
<React.Fragment>
<SchedulingComponentA />
<SchedulingComponentB />
</React.Fragment>
);
const SchedulingComponentA = () => {
const [count, setCount] = React.useState(0);
scheduleForA = () => setCount(prevCount => prevCount + 1);
return <Child count={count} />;
};
const SchedulingComponentB = () => {
const [count, setCount] = React.useState(0);
scheduleForB = () => setCount(prevCount => prevCount + 1);
return <Child count={count} />;
};
const Child = () => null;
await act(() => {
ReactDOM.render(<Parent />, document.createElement('div'));
});
expect(scheduleForA).not.toBeNull();
expect(scheduleForB).not.toBeNull();
expect(allSchedulerTypes).toEqual([[null]]);
await act(() => {
scheduleForA();
});
expect(allSchedulerTypes).toEqual([[null], [SchedulingComponentA]]);
await act(() => {
scheduleForB();
});
expect(allSchedulerTypes).toEqual([
[null],
[SchedulingComponentA],
[SchedulingComponentB],
]);
});
it('should report a class component as the scheduler for a setState update', async () => {
const Parent = () => <SchedulingComponent />;
class SchedulingComponent extends React.Component {
state = {};
render() {
instance = this;
return <Child />;
}
}
const Child = () => null;
let instance;
await act(() => {
ReactDOM.render(<Parent />, document.createElement('div'));
});
expect(allSchedulerTypes).toEqual([[null]]);
expect(instance).not.toBeNull();
await act(() => {
instance.setState({});
});
expect(allSchedulerTypes).toEqual([[null], [SchedulingComponent]]);
});
it('should cover cascading updates', async () => {
let triggerActiveCascade = null;
let triggerPassiveCascade = null;
const Parent = () => <SchedulingComponent />;
const SchedulingComponent = () => {
const [cascade, setCascade] = React.useState(null);
triggerActiveCascade = () => setCascade('active');
triggerPassiveCascade = () => setCascade('passive');
return <CascadingChild cascade={cascade} />;
};
const CascadingChild = ({cascade}) => {
const [count, setCount] = React.useState(0);
Scheduler.log(`CascadingChild ${count}`);
React.useLayoutEffect(() => {
if (cascade === 'active') {
setCount(prevCount => prevCount + 1);
}
return () => {};
}, [cascade]);
React.useEffect(() => {
if (cascade === 'passive') {
setCount(prevCount => prevCount + 1);
}
return () => {};
}, [cascade]);
return count;
};
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(async () => {
root.render(<Parent />);
await waitFor(['CascadingChild 0', 'onCommitRoot']);
});
expect(triggerActiveCascade).not.toBeNull();
expect(triggerPassiveCascade).not.toBeNull();
expect(allSchedulerTypes).toEqual([[null]]);
await act(async () => {
triggerActiveCascade();
await waitFor([
'CascadingChild 0',
'onCommitRoot',
'CascadingChild 1',
'onCommitRoot',
]);
});
expect(allSchedulerTypes).toEqual([
[null],
[SchedulingComponent],
[CascadingChild],
]);
await act(async () => {
triggerPassiveCascade();
await waitFor([
'CascadingChild 1',
'onCommitRoot',
'CascadingChild 2',
'onCommitRoot',
]);
});
expect(allSchedulerTypes).toEqual([
[null],
[SchedulingComponent],
[CascadingChild],
[SchedulingComponent],
[CascadingChild],
]);
// Verify no outstanding flushes
await waitForAll([]);
});
it('should cover suspense pings', async () => {
let data = null;
let resolver = null;
let promise = null;
const fakeCacheRead = () => {
if (data === null) {
promise = new Promise(resolve => {
resolver = resolvedData => {
data = resolvedData;
resolve(resolvedData);
};
});
throw promise;
} else {
return data;
}
};
const Parent = () => (
<React.Suspense fallback={<Fallback />}>
<Suspender />
</React.Suspense>
);
const Fallback = () => null;
let setShouldSuspend = null;
const Suspender = ({suspend}) => {
const tuple = React.useState(false);
setShouldSuspend = tuple[1];
if (tuple[0] === true) {
return fakeCacheRead();
} else {
return null;
}
};
await act(() => {
ReactDOM.render(<Parent />, document.createElement('div'));
assertLog(['onCommitRoot']);
});
expect(setShouldSuspend).not.toBeNull();
expect(allSchedulerTypes).toEqual([[null]]);
await act(() => {
setShouldSuspend(true);
});
assertLog(['onCommitRoot']);
expect(allSchedulerTypes).toEqual([[null], [Suspender]]);
expect(resolver).not.toBeNull();
await act(() => {
resolver('abc');
return promise;
});
assertLog(['onCommitRoot']);
expect(allSchedulerTypes).toEqual([[null], [Suspender], [Suspender]]);
// Verify no outstanding flushes
await waitForAll([]);
});
it('should cover error handling', async () => {
let triggerError = null;
const Parent = () => {
const [shouldError, setShouldError] = React.useState(false);
triggerError = () => setShouldError(true);
return shouldError ? (
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>
) : (
<ErrorBoundary>
<Yield value="initial" />
</ErrorBoundary>
);
};
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
if (this.state.error) {
return <Yield value="error" />;
}
return this.props.children;
}
}
const Yield = ({value}) => {
Scheduler.log(value);
return null;
};
const BrokenRender = () => {
throw new Error('Hello');
};
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Parent shouldError={false} />);
});
assertLog(['initial', 'onCommitRoot']);
expect(triggerError).not.toBeNull();
allSchedulerTypes.splice(0);
onCommitRootShouldYield = true;
await act(() => {
triggerError();
});
assertLog(['onCommitRoot', 'error', 'onCommitRoot']);
expect(allSchedulerTypes).toEqual([[Parent], [ErrorBoundary]]);
// Verify no outstanding flushes
await waitForAll([]);
});
it('should distinguish between updaters in the case of interleaved work', async () => {
const {
FunctionComponent,
HostRoot,
} = require('react-reconciler/src/ReactWorkTags');
let triggerLowPriorityUpdate = null;
let triggerSyncPriorityUpdate = null;
const SyncPriorityUpdater = () => {
const [count, setCount] = React.useState(0);
triggerSyncPriorityUpdate = () => setCount(prevCount => prevCount + 1);
Scheduler.log(`SyncPriorityUpdater ${count}`);
return <Yield value={`HighPriority ${count}`} />;
};
const LowPriorityUpdater = () => {
const [count, setCount] = React.useState(0);
triggerLowPriorityUpdate = () => {
React.startTransition(() => {
setCount(prevCount => prevCount + 1);
});
};
Scheduler.log(`LowPriorityUpdater ${count}`);
return <Yield value={`LowPriority ${count}`} />;
};
const Yield = ({value}) => {
Scheduler.log(`Yield ${value}`);
return null;
};
const root = ReactDOMClient.createRoot(document.createElement('div'));
root.render(
<React.Fragment>
<SyncPriorityUpdater />
<LowPriorityUpdater />
</React.Fragment>,
);
// Render everything initially.
await waitForAll([
'SyncPriorityUpdater 0',
'Yield HighPriority 0',
'LowPriorityUpdater 0',
'Yield LowPriority 0',
'onCommitRoot',
]);
expect(triggerLowPriorityUpdate).not.toBeNull();
expect(triggerSyncPriorityUpdate).not.toBeNull();
expect(allSchedulerTags).toEqual([[HostRoot]]);
// Render a partial update, but don't finish.
await act(async () => {
triggerLowPriorityUpdate();
await waitFor(['LowPriorityUpdater 1']);
expect(allSchedulerTags).toEqual([[HostRoot]]);
// Interrupt with higher priority work.
ReactDOM.flushSync(triggerSyncPriorityUpdate);
assertLog([
'SyncPriorityUpdater 1',
'Yield HighPriority 1',
'onCommitRoot',
]);
expect(allSchedulerTypes).toEqual([[null], [SyncPriorityUpdater]]);
// Finish the initial partial update
triggerLowPriorityUpdate();
await waitForAll([
'LowPriorityUpdater 2',
'Yield LowPriority 2',
'onCommitRoot',
]);
});
expect(allSchedulerTags).toEqual([
[HostRoot],
[FunctionComponent],
[FunctionComponent],
]);
expect(allSchedulerTypes).toEqual([
[null],
[SyncPriorityUpdater],
[LowPriorityUpdater],
]);
// Verify no outstanding flushes
await waitForAll([]);
});
});