Skip to content

Commit

Permalink
Adds a flag to disable legacy mode. Currently this flag is used to ca…
Browse files Browse the repository at this point in the history
…use 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 committed Feb 29, 2024
1 parent 01ab35a commit a913625
Show file tree
Hide file tree
Showing 43 changed files with 484 additions and 145 deletions.
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 @@ -452,6 +453,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 a913625

Please sign in to comment.