Skip to content

Commit

Permalink
[DOM] disable legacy mode behind flag (#28468)
Browse files Browse the repository at this point in the history
Adds a flag to disable legacy mode. Currently this flag is used to cause
legacy mode apis like render and hydrate to throw. This change also
removes render, hydrate, unmountComponentAtNode, and
unstable_renderSubtreeIntoContainer from the experiemntal entrypoint.
Right now for Meta builds this flag is off (legacy mode is still
supported). In OSS builds this flag matches __NEXT_MAJOR__ which means
it currently is on in experiemental. This means that after merging
legacy mode is effectively removed from experimental builds. While this
is a breaking change, experimental builds are not stable and users can
pin to older versions or update their use of react-dom to no longer use
legacy mode APIs.
  • Loading branch information
gnoff authored Mar 4, 2024
1 parent 034130c commit 1c02b9d
Show file tree
Hide file tree
Showing 46 changed files with 483 additions and 187 deletions.
5 changes: 5 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ describe('Store', () => {

// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support mount and update operations for multiple roots (legacy render)', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
Expand Down Expand Up @@ -941,6 +942,7 @@ describe('Store', () => {

// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support mount and update operations for multiple roots (legacy render)', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
Expand Down Expand Up @@ -1469,6 +1471,7 @@ describe('Store', () => {

// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('detects and updates profiling support based on the attached roots (legacy render)', () => {
const Component = () => null;

Expand Down Expand Up @@ -1632,6 +1635,7 @@ describe('Store', () => {

// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support Lazy components (legacy render)', async () => {
const container = document.createElement('div');

Expand Down Expand Up @@ -1702,6 +1706,7 @@ describe('Store', () => {

// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support Lazy components that are unmounted before they finish loading (legacy render)', async () => {
const container = document.createElement('div');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('StoreStress (Legacy Mode)', () => {
// It renders different trees that should produce the same output.
// @reactVersion >= 16.9
// @reactVersion < 19
// @gate !disableLegacyMode
it('should handle a stress test with different tree operations (Legacy Mode)', () => {
let setShowX;
const A = () => 'a';
Expand Down
4 changes: 0 additions & 4 deletions packages/react-dom/index.experimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ export {
hydrateRoot,
findDOMNode,
flushSync,
hydrate,
render,
unmountComponentAtNode,
unstable_batchedUpdates,
unstable_renderSubtreeIntoContainer,
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
useFormStatus,
useFormState,
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/__tests__/ReactComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('ReactComponent', () => {
act = require('internal-test-utils').act;
});

// @gate !disableLegacyMode
it('should throw on invalid render targets in legacy roots', () => {
const container = document.createElement('div');
// jQuery objects are basically arrays; people often pass them in by mistake
Expand Down Expand Up @@ -455,6 +456,7 @@ describe('ReactComponent', () => {
/* eslint-enable indent */
});

// @gate !disableLegacyMode
it('fires the callback after a component is rendered in legacy roots', () => {
const callback = jest.fn();
const container = document.createElement('div');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ describe('ReactCompositeComponent-state', () => {
);
});

// @gate !disableLegacyMode
it('Legacy mode should support setState in componentWillUnmount (#18851)', () => {
let subscription;
class A extends React.Component {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ describe('ReactDOM', () => {
expect(dog.className).toBe('bigdog');
});

// @gate !disableLegacyMode
it('throws in render() if the mount callback in legacy roots is not a function', async () => {
function Foo() {
this.a = 1;
Expand Down Expand Up @@ -216,6 +217,7 @@ describe('ReactDOM', () => {
);
});

// @gate !disableLegacyMode
it('throws in render() if the update callback in legacy roots is not a function', async () => {
function Foo() {
this.a = 1;
Expand Down
22 changes: 13 additions & 9 deletions packages/react-dom/src/__tests__/ReactDOMComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ describe('ReactDOMComponent', () => {
});
});

it('throws with Temporal-like objects as style values', () => {
it('throws with Temporal-like objects as style values', async () => {
class TemporalLike {
valueOf() {
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
Expand All @@ -344,14 +344,17 @@ describe('ReactDOMComponent', () => {
}
}
const style = {fontSize: new TemporalLike()};
const div = document.createElement('div');
const test = () => ReactDOM.render(<span style={style} />, div);
expect(() =>
expect(test).toThrowError(new TypeError('prod message')),
).toErrorDev(
'Warning: The provided `fontSize` CSS property is an unsupported type TemporalLike.' +
' This value must be coerced to a string before using it here.',
);
const root = ReactDOMClient.createRoot(document.createElement('div'));
await expect(async () => {
await expect(async () => {
await act(() => {
root.render(<span style={style} />);
});
}).toErrorDev(
'Warning: The provided `fontSize` CSS property is an unsupported type TemporalLike.' +
' This value must be coerced to a string before using it here.',
);
}).rejects.toThrowError(new TypeError('prod message'));
});

it('should update styles if initially null', async () => {
Expand Down Expand Up @@ -3688,6 +3691,7 @@ describe('ReactDOMComponent', () => {
expect(typeof portalContainer.onclick).toBe('function');
});

// @gate !disableLegacyMode
it('does not add onclick handler to the React root in legacy mode', () => {
const container = document.createElement('div');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
});

describe('ReactDOM.render', () => {
// @gate !disableLegacyMode
it('logs errors during event handlers', async () => {
spyOnDevAndProd(console, 'error');

Expand Down Expand Up @@ -156,6 +157,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});

// @gate !disableLegacyMode
it('logs render errors without an error boundary', async () => {
spyOnDevAndProd(console, 'error');

Expand Down Expand Up @@ -223,6 +225,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});

// @gate !disableLegacyMode
it('logs render errors with an error boundary', async () => {
spyOnDevAndProd(console, 'error');

Expand Down Expand Up @@ -295,6 +298,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});

// @gate !disableLegacyMode
it('logs layout effect errors without an error boundary', async () => {
spyOnDevAndProd(console, 'error');

Expand Down Expand Up @@ -365,6 +369,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});

// @gate !disableLegacyMode
it('logs layout effect errors with an error boundary', async () => {
spyOnDevAndProd(console, 'error');

Expand Down Expand Up @@ -440,6 +445,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});

// @gate !disableLegacyMode
it('logs passive effect errors without an error boundary', async () => {
spyOnDevAndProd(console, 'error');

Expand Down Expand Up @@ -511,6 +517,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});

// @gate !disableLegacyMode
it('logs passive effect errors with an error boundary', async () => {
spyOnDevAndProd(console, 'error');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('ReactDOMFiberAsync', () => {
document.body.removeChild(container);
});

// @gate !disableLegacyMode
it('renders synchronously by default in legacy mode', () => {
const ops = [];
ReactDOM.render(<div>Hi</div>, container, () => {
Expand Down
45 changes: 45 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMHooks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('ReactDOMHooks', () => {
document.body.removeChild(container);
});

// @gate !disableLegacyMode
it('can ReactDOM.render() from useEffect', async () => {
const container2 = document.createElement('div');
const container3 = document.createElement('div');
Expand Down Expand Up @@ -76,6 +77,50 @@ describe('ReactDOMHooks', () => {
expect(container3.textContent).toBe('6');
});

it('can render() from useEffect', async () => {
const container2 = document.createElement('div');
const container3 = document.createElement('div');

const root1 = ReactDOMClient.createRoot(container);
const root2 = ReactDOMClient.createRoot(container2);
const root3 = ReactDOMClient.createRoot(container3);

function Example1({n}) {
React.useEffect(() => {
root2.render(<Example2 n={n} />);
});
return 1 * n;
}

function Example2({n}) {
React.useEffect(() => {
root3.render(<Example3 n={n} />);
});
return 2 * n;
}

function Example3({n}) {
return 3 * n;
}

await act(() => {
root1.render(<Example1 n={1} />);
});
await waitForAll([]);
expect(container.textContent).toBe('1');
expect(container2.textContent).toBe('2');
expect(container3.textContent).toBe('3');

await act(() => {
root1.render(<Example1 n={2} />);
});
await waitForAll([]);
expect(container.textContent).toBe('2');
expect(container2.textContent).toBe('4');
expect(container3.textContent).toBe('6');
});

// @gate !disableLegacyMode
it('should not bail out when an update is scheduled from within an event handler', () => {
const {createRef, useCallback, useState} = React;

Expand Down
24 changes: 11 additions & 13 deletions packages/react-dom/src/__tests__/ReactDOMInput-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ describe('ReactDOMInput', () => {
expect(node.value).toBe('foobar');
});

it('should throw for date inputs if `defaultValue` is an object where valueOf() throws', () => {
it('should throw for date inputs if `defaultValue` is an object where valueOf() throws', async () => {
class TemporalLike {
valueOf() {
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
Expand All @@ -744,19 +744,16 @@ describe('ReactDOMInput', () => {
return '2020-01-01';
}
}
const legacyContainer = document.createElement('div');
document.body.appendChild(legacyContainer);
const test = () =>
ReactDOM.render(
<input defaultValue={new TemporalLike()} type="date" />,
legacyContainer,
await expect(async () => {
await expect(async () => {
await act(() => {
root.render(<input defaultValue={new TemporalLike()} type="date" />);
});
}).toErrorDev(
'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
'strings, not TemporalLike. This value must be coerced to a string before using it here.',
);
expect(() =>
expect(test).toThrowError(new TypeError('prod message')),
).toErrorDev(
'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
'strings, not TemporalLike. This value must be coerced to a string before using it here.',
);
}).rejects.toThrowError(new TypeError('prod message'));
});

it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', async () => {
Expand Down Expand Up @@ -1736,6 +1733,7 @@ describe('ReactDOMInput', () => {
assertInputTrackingIsCurrent(container);
});

// @gate !disableLegacyMode
it('should control radio buttons if the tree updates during render in legacy mode', async () => {
container.remove();
container = document.createElement('div');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('ReactDOMComponentTree', () => {
container = null;
});

// @gate !disableLegacyMode
it('finds instance of node that is attempted to be unmounted', () => {
const component = <div />;
const node = ReactDOM.render(<div>{component}</div>, container);
Expand All @@ -39,6 +40,7 @@ describe('ReactDOMComponentTree', () => {
);
});

// @gate !disableLegacyMode
it('finds instance from node to stop rendering over other react rendered components', () => {
const component = (
<div>
Expand Down
Loading

0 comments on commit 1c02b9d

Please sign in to comment.