diff --git a/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js b/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js index d04783b6a7e33..906683c0975fb 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js @@ -58,5 +58,9 @@ export default function UnserializableProps(): React.Node { } function ChildComponent(props: any) { - return null; + return ( + <> +
{props.bigInt}
+ + ); } diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index cc773dea4de36..24462c1d2bb5a 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -66,6 +66,7 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO import sanitizeURL from '../shared/sanitizeURL'; import { + enableBigIntSupport, enableCustomElementPropertySupport, enableClientRenderFallbackOnTextMismatch, enableFormActions, @@ -326,7 +327,7 @@ function normalizeMarkupForTextOrAttribute(markup: mixed): string { export function checkForUnmatchedText( serverText: string, - clientText: string | number, + clientText: string | number | bigint, isConcurrentMode: boolean, shouldWarnDev: boolean, ) { @@ -397,12 +398,17 @@ function setProp( if (canSetTextContent) { setTextContent(domElement, value); } - } else if (typeof value === 'number') { + } else if ( + typeof value === 'number' || + (enableBigIntSupport && typeof value === 'bigint') + ) { if (__DEV__) { + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint validateTextNesting('' + value, tag); } const canSetTextContent = tag !== 'body'; if (canSetTextContent) { + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint setTextContent(domElement, '' + value); } } @@ -955,7 +961,11 @@ function setPropOnCustomElement( case 'children': { if (typeof value === 'string') { setTextContent(domElement, value); - } else if (typeof value === 'number') { + } else if ( + typeof value === 'number' || + (enableBigIntSupport && typeof value === 'bigint') + ) { + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint setTextContent(domElement, '' + value); } break; @@ -2817,7 +2827,12 @@ export function diffHydratedProperties( // even listeners these nodes might be wired up to. // TODO: Warn if there is more than a single textNode as a child. // TODO: Should we use domElement.firstChild.nodeValue to compare? - if (typeof children === 'string' || typeof children === 'number') { + if ( + typeof children === 'string' || + typeof children === 'number' || + (enableBigIntSupport && typeof children === 'bigint') + ) { + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint if (domElement.textContent !== '' + children) { if (props.suppressHydrationWarning !== true) { checkForUnmatchedText( diff --git a/packages/react-dom-bindings/src/client/ReactDOMOption.js b/packages/react-dom-bindings/src/client/ReactDOMOption.js index 4f7d3105a8ef5..10d04bd6f7eb2 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMOption.js +++ b/packages/react-dom-bindings/src/client/ReactDOMOption.js @@ -8,6 +8,7 @@ */ import {Children} from 'react'; +import {enableBigIntSupport} from 'shared/ReactFeatureFlags'; let didWarnSelectedSetOnOption = false; let didWarnInvalidChild = false; @@ -26,7 +27,11 @@ export function validateOptionProps(element: Element, props: Object) { if (child == null) { return; } - if (typeof child === 'string' || typeof child === 'number') { + if ( + typeof child === 'string' || + typeof child === 'number' || + (enableBigIntSupport && typeof child === 'bigint') + ) { return; } if (!didWarnInvalidChild) { diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 9e2c5e41c062f..973a9fb931246 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -89,6 +89,7 @@ import { import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying'; import { + enableBigIntSupport, enableCreateEventHandleAPI, enableScopeAPI, enableFloat, @@ -548,6 +549,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean { type === 'noscript' || typeof props.children === 'string' || typeof props.children === 'number' || + (enableBigIntSupport && typeof props.children === 'bigint') || (typeof props.dangerouslySetInnerHTML === 'object' && props.dangerouslySetInnerHTML !== null && props.dangerouslySetInnerHTML.__html != null) diff --git a/packages/react-dom-bindings/src/client/ToStringValue.js b/packages/react-dom-bindings/src/client/ToStringValue.js index 74a9b5b4bca7f..e1fc51b775dd4 100644 --- a/packages/react-dom-bindings/src/client/ToStringValue.js +++ b/packages/react-dom-bindings/src/client/ToStringValue.js @@ -8,10 +8,12 @@ */ import {checkFormFieldValueStringCoercion} from 'shared/CheckStringCoercion'; +import {enableBigIntSupport} from 'shared/ReactFeatureFlags'; export opaque type ToStringValue = | boolean | number + | bigint | Object | string | null @@ -28,6 +30,12 @@ export function toString(value: ToStringValue): string { export function getToStringValue(value: mixed): ToStringValue { switch (typeof value) { + case 'bigint': + if (!enableBigIntSupport) { + // bigint is assigned as empty string + return ''; + } + // fallthrough for BigInt support case 'boolean': case 'number': case 'string': diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 411e81180d9cf..a469b5587d79a 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -28,6 +28,7 @@ import { import {Children} from 'react'; import { + enableBigIntSupport, enableFilterEmptyStringAttributesDOM, enableCustomElementPropertySupport, enableFloat, @@ -1626,7 +1627,9 @@ function flattenOptionChildren(children: mixed): string { if ( !didWarnInvalidOptionChildren && typeof child !== 'string' && - typeof child !== 'number' + typeof child !== 'number' && + ((enableBigIntSupport && typeof child !== 'bigint') || + !enableBigIntSupport) ) { didWarnInvalidOptionChildren = true; console.error( @@ -2960,36 +2963,40 @@ function pushTitle( if (Array.isArray(children) && children.length > 1) { console.error( - 'React expects the `children` prop of tags to be a string, number, or object with a novel `toString` method but found an Array with length %s instead.' + + 'React expects the `children` prop of <title> tags to be a string, number%s, or object with a novel `toString` method but found an Array with length %s instead.' + ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value' + ' which is why Arrays of length greater than 1 are not supported. When using JSX it can be commong to combine text nodes and value nodes.' + ' For example: <title>hello {nameOfUser}. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop' + ' is using this form try rewriting it using a template string: {`hello ${nameOfUser}`}.', + enableBigIntSupport ? ', bigint' : '', children.length, ); } else if (typeof child === 'function' || typeof child === 'symbol') { const childType = typeof child === 'function' ? 'a Function' : 'a Sybmol'; console.error( - 'React expect children of tags to be a string, number, or object with a novel `toString` method but found %s instead.' + + 'React expect children of <title> tags to be a string, number%s, or object with a novel `toString` method but found %s instead.' + ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title>' + ' tags to a single string value.', + enableBigIntSupport ? ', bigint' : '', childType, ); } else if (child && child.toString === {}.toString) { if (child.$$typeof != null) { console.error( - 'React expects the `children` prop of <title> tags to be a string, number, or object with a novel `toString` method but found an object that appears to be' + + 'React expects the `children` prop of <title> tags to be a string, number%s, or object with a novel `toString` method but found an object that appears to be' + ' a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to' + ' be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is' + ' a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.', + enableBigIntSupport ? ', bigint' : '', ); } else { console.error( - 'React expects the `children` prop of <title> tags to be a string, number, or object with a novel `toString` method but found an object that does not implement' + + 'React expects the `children` prop of <title> tags to be a string, number%s, or object with a novel `toString` method but found an object that does not implement' + ' a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags' + ' to a single string value. Using the default `toString` method available on every object is almost certainly an error. Consider whether the `children` of this <title>' + ' is an object in error and change it to a string or number value if so. Otherwise implement a `toString` method that React can use to produce a valid <title>.', + enableBigIntSupport ? ', bigint' : '', ); } } @@ -3123,14 +3130,17 @@ function pushStartTitle( } else if ( childForValidation != null && typeof childForValidation !== 'string' && - typeof childForValidation !== 'number' + typeof childForValidation !== 'number' && + ((enableBigIntSupport && typeof childForValidation !== 'bigint') || + !enableBigIntSupport) ) { console.error( - 'A title element received a value that was not a string or number for children. ' + + 'A title element received a value that was not a string or number%s for children. ' + 'In the browser title Elements can only have Text Nodes as children. If ' + 'the children being rendered output more than a single text node in aggregate the browser ' + 'will display markup and comments as text in the title and hydration will likely fail and ' + 'fall back to client rendering', + enableBigIntSupport ? ' or bigint' : '', ); } } diff --git a/packages/react-dom-bindings/src/server/escapeTextForBrowser.js b/packages/react-dom-bindings/src/server/escapeTextForBrowser.js index 842d1b1328fdb..6fd43c01c5f69 100644 --- a/packages/react-dom-bindings/src/server/escapeTextForBrowser.js +++ b/packages/react-dom-bindings/src/server/escapeTextForBrowser.js @@ -39,6 +39,7 @@ */ import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion'; +import {enableBigIntSupport} from 'shared/ReactFeatureFlags'; const matchHtmlRegExp = /["'&<>]/; @@ -106,7 +107,11 @@ function escapeHtml(string: string) { * @return {string} An escaped string. */ function escapeTextForBrowser(text: string | number | boolean): string { - if (typeof text === 'boolean' || typeof text === 'number') { + if ( + typeof text === 'boolean' || + typeof text === 'number' || + (enableBigIntSupport && typeof text === 'bigint') + ) { // this shortcircuit helps perf for types that we know will never have // special characters, especially given that this function is used often // for numeric dom ids. diff --git a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js index 94172ab77b237..2882cab2e9991 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js @@ -60,6 +60,17 @@ describe('ReactDOMFiber', () => { expect(container.textContent).toEqual('10'); }); + // @gate enableBigIntSupport + it('should render bigints as children', async () => { + const Box = ({value}) => <div>{value}</div>; + + await act(async () => { + root.render(<Box value={10n} />); + }); + + expect(container.textContent).toEqual('10'); + }); + it('should call an effect after mount/update (replacing render callback pattern)', async () => { function Component() { React.useEffect(() => { diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 5a796c5446f06..ac7ddfd2f5576 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3373,6 +3373,17 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate enableBigIntSupport + it('Supports bigint', async () => { + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + <div>{10n}</div>, + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual(<div>10</div>); + }); + it('Supports custom abort reasons with a string', async () => { function App() { return ( @@ -5642,6 +5653,60 @@ describe('ReactDOMFizzServer', () => { expect(getVisibleChildren(document.head)).toEqual(<title>hello); }); + it('should accept a single number child', async () => { + // a Single number child + function App() { + return ( + + 4 + + ); + } + + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); + expect(getVisibleChildren(document.head)).toEqual(4); + + const errors = []; + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + errors.push(error.message); + }, + }); + await waitForAll([]); + expect(errors).toEqual([]); + expect(getVisibleChildren(document.head)).toEqual(4); + }); + + it('should accept a single bigint child', async () => { + // a Single number child + function App() { + return ( + + 5n + + ); + } + + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); + expect(getVisibleChildren(document.head)).toEqual(5n); + + const errors = []; + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + errors.push(error.message); + }, + }); + await waitForAll([]); + expect(errors).toEqual([]); + expect(getVisibleChildren(document.head)).toEqual(5n); + }); + it('should accept children array of length 1 containing a string', async () => { // a Single string child function App() { @@ -5684,7 +5749,9 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); }).toErrorDev([ - 'React expects the `children` prop of tags to be a string, number, or object with a novel `toString` method but found an Array with length 2 instead. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value which is why Arrays of length greater than 1 are not supported. When using JSX it can be commong to combine text nodes and value nodes. For example: <title>hello {nameOfUser}. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop is using this form try rewriting it using a template string: {`hello ${nameOfUser}`}.', + 'React expects the `children` prop of tags to be a string, number' + + gate(flags => (flags.enableBigIntSupport ? ', bigint' : '')) + + ', or object with a novel `toString` method but found an Array with length 2 instead. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value which is why Arrays of length greater than 1 are not supported. When using JSX it can be commong to combine text nodes and value nodes. For example: <title>hello {nameOfUser}. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop is using this form try rewriting it using a template string: {`hello ${nameOfUser}`}.', ]); if (gate(flags => flags.enableFloat)) { @@ -5744,7 +5811,67 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); }).toErrorDev([ - 'React expects the `children` prop of tags to be a string, number, or object with a novel `toString` method but found an object that appears to be a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.', + 'React expects the `children` prop of <title> tags to be a string, number' + + gate(flags => (flags.enableBigIntSupport ? ', bigint' : '')) + + ', or object with a novel `toString` method but found an object that appears to be a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.', + ]); + } else { + await expect(async () => { + await act(() => { + const {pipe} = renderToPipeableStream(<App />); + pipe(writable); + }); + }).toErrorDev([ + 'A title element received a React element for children. In the browser title Elements can only have Text Nodes as children. If the children being rendered output more than a single text node in aggregate the browser will display markup and comments as text in the title and hydration will likely fail and fall back to client rendering', + ]); + } + + if (gate(flags => flags.enableFloat)) { + // object titles are toStringed when float is on + expect(getVisibleChildren(document.head)).toEqual( + <title>{'[object Object]'}, + ); + } else { + expect(getVisibleChildren(document.head)).toEqual(hello); + } + + const errors = []; + ReactDOMClient.hydrateRoot(document.head, , { + onRecoverableError(error) { + errors.push(error.message); + }, + }); + await waitForAll([]); + expect(errors).toEqual([]); + if (gate(flags => flags.enableFloat)) { + // object titles are toStringed when float is on + expect(getVisibleChildren(document.head)).toEqual( + {'[object Object]'}, + ); + } else { + expect(getVisibleChildren(document.head)).toEqual(hello); + } + }); + + it('should warn in dev if you pass an object that does not implement toString as a child to ', async () => { + function App() { + return ( + <head> + <title>{{}} + + ); + } + + if (gate(flags => flags.enableFloat)) { + await expect(async () => { + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); + }).toErrorDev([ + 'React expects the `children` prop of tags to be a string, number' + + gate(flags => (flags.enableBigIntSupport ? ', bigint' : '')) + + ', or object with a novel `toString` method but found an object that does not implement a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags to a single string value. Using the default `toString` method available on every object is almost certainly an error. Consider whether the `children` of this <title> is an object in error and change it to a string or number value if so. Otherwise implement a `toString` method that React can use to produce a valid <title>.', ]); } else { await expect(async () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 7d1ed0e21d471..ad7342f53398d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -657,6 +657,16 @@ describe('ReactDOMInput', () => { expect(div.firstChild.getAttribute('defaultValue')).toBe(null); }); + it('should render bigint defaultValue for SSR', () => { + const markup = ReactDOMServer.renderToString( + <input type="text" defaultValue={5n} />, + ); + const div = document.createElement('div'); + div.innerHTML = markup; + expect(div.firstChild.getAttribute('value')).toBe('5'); + expect(div.firstChild.getAttribute('defaultValue')).toBe(null); + }); + it('should render value for SSR', () => { const element = <input type="text" value="1" onChange={() => {}} />; const markup = ReactDOMServer.renderToString(element); @@ -666,6 +676,15 @@ describe('ReactDOMInput', () => { expect(div.firstChild.getAttribute('defaultValue')).toBe(null); }); + it('should render bigint value for SSR', () => { + const element = <input type="text" value={5n} onChange={() => {}} />; + const markup = ReactDOMServer.renderToString(element); + const div = document.createElement('div'); + div.innerHTML = markup; + expect(div.firstChild.getAttribute('value')).toBe('5'); + expect(div.firstChild.getAttribute('defaultValue')).toBe(null); + }); + it('should render name attribute if it is supplied', async () => { await act(() => { root.render(<input type="text" name="name" />); @@ -830,6 +849,16 @@ describe('ReactDOMInput', () => { expect(node.value).toBe('0'); }); + // @gate enableBigIntSupport + it('should display `value` of bigint 5', async () => { + await act(() => { + root.render(<input type="text" value={5n} onChange={emptyFunction} />); + }); + const node = container.firstChild; + + expect(node.value).toBe('5'); + }); + it('should allow setting `value` to `true`', async () => { await act(() => { root.render(<input type="text" value="yolo" onChange={emptyFunction} />); diff --git a/packages/react-dom/src/__tests__/ReactDOMOption-test.js b/packages/react-dom/src/__tests__/ReactDOMOption-test.js index abeae80125bdf..a75ce875001f6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMOption-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMOption-test.js @@ -172,6 +172,13 @@ describe('ReactDOMOption', () => { expect(node.value).toBe('hello'); }); + // @gate enableBigIntSupport + it('should support bigint values', () => { + const node = ReactTestUtils.renderIntoDocument(<option>{5n}</option>); + expect(node.innerHTML).toBe('5'); + expect(node.value).toBe('5'); + }); + it('should be able to use dangerouslySetInnerHTML on option', () => { const stub = <option dangerouslySetInnerHTML={{__html: 'foobar'}} />; let node; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js index 5589e5dc3a220..266bae3d5eeaa 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js @@ -72,6 +72,16 @@ describe('ReactDOMServerIntegration', () => { expect(e.nodeValue).toMatch('42'); }); + itRenders('a bigint', async render => { + const e = await render(42n); + if (gate(flags => flags.enableBigIntSupport)) { + expect(e.nodeType).toBe(3); + expect(e.nodeValue).toMatch('42'); + } else { + expect(e).toBe(null); + } + }); + itRenders('an array with one child', async render => { const e = await render([<div key={1}>text1</div>]); const parent = e.parentNode; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js index 4abf637a53713..3ad7abf91a7df 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js @@ -32,7 +32,8 @@ function initModules() { }; } -const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); +const {resetModules, itRenders, serverRender, streamRender} = + ReactDOMServerIntegrationUtils(initModules); // TODO: Run this in React Fire mode after we figure out the SSR behavior. const desc = disableInputAttributeSyncing ? xdescribe : describe; @@ -46,6 +47,18 @@ desc('ReactDOMServerIntegrationInput', () => { expect(e.value).toBe('foo'); }); + itRenders('an input with a bigint value and an onChange', async render => { + console.log(gate(flags => flags.enableBigIntSupport)); + const e = await render(<input value={5n} onChange={() => {}} />); + expect(e.value).toBe( + gate(flags => flags.enableBigIntSupport) || + render === serverRender || + render === streamRender + ? '5' + : '', + ); + }); + itRenders('an input with a value and readOnly', async render => { const e = await render(<input value="foo" readOnly={true} />); expect(e.value).toBe('foo'); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js index 8bf3c7c3d20da..13c7f78ea831e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js @@ -218,11 +218,15 @@ describe('ReactDOMServerIntegrationSelect', () => { itRenders('a select option with flattened children', async render => { const e = await render( <select value="bar" readOnly={true}> - <option value="bar">A {'B'}</option> + <option value="bar"> + A {'B'} {5n} + </option> </select>, ); const option = e.options[0]; - expect(option.textContent).toBe('A B'); + expect(option.textContent).toBe( + gate(flags => flags.enableBigIntSupport) ? 'A B 5' : 'A B ', + ); expect(option.value).toBe('bar'); expect(option.selected).toBe(true); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js index ff69355812d3c..79b5fd840d88a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js @@ -30,7 +30,8 @@ function initModules() { }; } -const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); +const {resetModules, itRenders, serverRender, streamRender} = + ReactDOMServerIntegrationUtils(initModules); describe('ReactDOMServerIntegrationTextarea', () => { beforeEach(() => { @@ -47,12 +48,23 @@ describe('ReactDOMServerIntegrationTextarea', () => { expect(e.value).toBe('foo'); }); + itRenders('a textarea with a bigint value and an onChange', async render => { + const e = await render(<textarea value={5n} onChange={() => {}} />); + expect(e.getAttribute('value')).toBe(null); + expect(e.value).toBe( + gate(flags => flags.enableBigIntSupport) || + render === serverRender || + render === streamRender + ? '5' + : '', + ); + }); + itRenders('a textarea with a value of undefined', async render => { const e = await render(<textarea value={undefined} />); expect(e.getAttribute('value')).toBe(null); expect(e.value).toBe(''); }); - itRenders('a textarea with a value and readOnly', async render => { const e = await render(<textarea value="foo" readOnly={true} />); // textarea DOM elements don't have a value **attribute**, the text is diff --git a/packages/react-dom/src/__tests__/ReactDOMTextarea-test.js b/packages/react-dom/src/__tests__/ReactDOMTextarea-test.js index 3d21ca64e399e..88c574b4cb2e5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMTextarea-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMTextarea-test.js @@ -80,6 +80,19 @@ describe('ReactDOMTextarea', () => { expect(node.value).toBe('0'); }); + // @gate enableBigIntSupport + it('should display `defaultValue` of bigint 0', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + const node = await renderTextarea( + <textarea defaultValue={0n} />, + container, + root, + ); + + expect(node.value).toBe('0'); + }); + it('should display "false" for `defaultValue` of `false`', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); diff --git a/packages/react-dom/src/__tests__/ReactMultiChildText-test.js b/packages/react-dom/src/__tests__/ReactMultiChildText-test.js index 450d0383eb0fc..7cf2c048c504e 100644 --- a/packages/react-dom/src/__tests__/ReactMultiChildText-test.js +++ b/packages/react-dom/src/__tests__/ReactMultiChildText-test.js @@ -58,6 +58,7 @@ const expectChildren = function (container, children) { continue; } textNode = outerNode.childNodes[mountIndex]; + expect(textNode != null).toBe(true); expect(textNode.nodeType).toBe(3); expect(textNode.data).toBe(child); mountIndex++; @@ -173,6 +174,15 @@ describe('ReactMultiChildText', () => { ]); }); + // @gate enableBigIntSupport + it('should correctly handle bigint children for render and update', async () => { + // prettier-ignore + await testAllPermutations([ + 10n, '10', + [10n], ['10'] + ]); + }); + it('should throw if rendering both HTML and children', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 77c386457e1d2..0470672f4b5f3 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -272,7 +272,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { throw new Error('Error in host config.'); } return ( - typeof props.children === 'string' || typeof props.children === 'number' + typeof props.children === 'string' || + typeof props.children === 'number' || + typeof props.children === 'bigint' ); } @@ -828,7 +830,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return childToJSX(child[0], null); } const children = child.map(c => childToJSX(c, null)); - if (children.every(c => typeof c === 'string' || typeof c === 'number')) { + if ( + children.every( + c => + typeof c === 'string' || + typeof c === 'number' || + typeof c === 'bigint', + ) + ) { return children.join(''); } return children; diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 118162ae36269..41cdf9a02f2ff 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -25,6 +25,7 @@ import { Forked, PlacementDEV, } from './ReactFiberFlags'; +import {enableBigIntSupport} from 'shared/ReactFeatureFlags'; import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -665,12 +666,14 @@ function createChildReconciler( ): Fiber | null { if ( (typeof newChild === 'string' && newChild !== '') || - typeof newChild === 'number' + typeof newChild === 'number' || + (enableBigIntSupport && typeof newChild === 'bigint') ) { // Text nodes don't have keys. If the previous node is implicitly keyed // we can continue to replace it without aborting even if it is not a text // node. const created = createFiberFromText( + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint '' + newChild, returnFiber.mode, lanes, @@ -785,7 +788,8 @@ function createChildReconciler( if ( (typeof newChild === 'string' && newChild !== '') || - typeof newChild === 'number' + typeof newChild === 'number' || + (enableBigIntSupport && typeof newChild === 'bigint') ) { // Text nodes don't have keys. If the previous node is implicitly keyed // we can continue to replace it without aborting even if it is not a text @@ -796,6 +800,7 @@ function createChildReconciler( return updateTextNode( returnFiber, oldFiber, + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint '' + newChild, lanes, debugInfo, @@ -908,7 +913,8 @@ function createChildReconciler( ): Fiber | null { if ( (typeof newChild === 'string' && newChild !== '') || - typeof newChild === 'number' + typeof newChild === 'number' || + (enableBigIntSupport && typeof newChild === 'bigint') ) { // Text nodes don't have keys, so we neither have to check the old nor // new node for the key. If both are text nodes, they match. @@ -916,6 +922,7 @@ function createChildReconciler( return updateTextNode( returnFiber, matchedFiber, + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint '' + newChild, lanes, debugInfo, @@ -1723,12 +1730,14 @@ function createChildReconciler( if ( (typeof newChild === 'string' && newChild !== '') || - typeof newChild === 'number' + typeof newChild === 'number' || + (enableBigIntSupport && typeof newChild === 'bigint') ) { return placeSingleChild( reconcileSingleTextNode( returnFiber, currentFirstChild, + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint '' + newChild, lanes, ), diff --git a/packages/react-reconciler/src/__tests__/ReactTopLevelText-test.js b/packages/react-reconciler/src/__tests__/ReactTopLevelText-test.js index 30a3fa093b4ab..9c5de7bd2ee5f 100644 --- a/packages/react-reconciler/src/__tests__/ReactTopLevelText-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTopLevelText-test.js @@ -39,4 +39,12 @@ describe('ReactTopLevelText', () => { await waitForAll([]); expect(ReactNoop).toMatchRenderedOutput('10'); }); + + // @gate enableBigIntSupport + it('should render a component returning bigints directly from render', async () => { + const Text = ({value}) => value; + ReactNoop.render(<Text value={10n} />); + await waitForAll([]); + expect(ReactNoop).toMatchRenderedOutput('10'); + }); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 7930aad88216f..e17b58826fb83 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -139,6 +139,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import { disableLegacyContext, disableModulePatternComponents, + enableBigIntSupport, enableScopeAPI, enableSuspenseAvoidThisFallbackFizz, enableFloat, @@ -2364,7 +2365,10 @@ function renderNodeDestructive( return; } - if (typeof node === 'number') { + if ( + typeof node === 'number' || + (enableBigIntSupport && typeof node === 'bigint') + ) { const segment = task.blockedSegment; if (segment === null) { // We assume a text node doesn't have a representation in the replay set, diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js index 8b9219b9fb1f0..d3c155dc8be77 100644 --- a/packages/react/src/ReactChildren.js +++ b/packages/react/src/ReactChildren.js @@ -16,6 +16,7 @@ import type { } from 'shared/ReactTypes'; import isArray from 'shared/isArray'; +import {enableBigIntSupport} from 'shared/ReactFeatureFlags'; import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -163,6 +164,11 @@ function mapIntoArray( invokeCallback = true; } else { switch (type) { + case 'bigint': + if (!enableBigIntSupport) { + break; + } + // fallthrough for enabled BigInt support case 'string': case 'number': invokeCallback = true; diff --git a/packages/react/src/__tests__/ReactChildren-test.js b/packages/react/src/__tests__/ReactChildren-test.js index 016e4089d8d3a..532cc3e8ec232 100644 --- a/packages/react/src/__tests__/ReactChildren-test.js +++ b/packages/react/src/__tests__/ReactChildren-test.js @@ -183,11 +183,14 @@ describe('ReactChildren', () => { {false} {null} {undefined} + {9n} </div> ); function assertCalls() { - expect(callback).toHaveBeenCalledTimes(9); + expect(callback).toHaveBeenCalledTimes( + gate(flags => flags.enableBigIntSupport) ? 10 : 9, + ); expect(callback).toHaveBeenCalledWith(div, 0); expect(callback).toHaveBeenCalledWith(span, 1); expect(callback).toHaveBeenCalledWith(a, 2); @@ -197,6 +200,11 @@ describe('ReactChildren', () => { expect(callback).toHaveBeenCalledWith(null, 6); expect(callback).toHaveBeenCalledWith(null, 7); expect(callback).toHaveBeenCalledWith(null, 8); + if (gate(flags => flags.enableBigIntSupport)) { + expect(callback).toHaveBeenCalledWith(9n, 9); + } else { + expect(callback).not.toHaveBeenCalledWith(9n, 9); + } callback.mockClear(); } @@ -209,13 +217,24 @@ describe('ReactChildren', () => { context, ); assertCalls(); - expect(mappedChildren).toEqual([ - <div key=".$divNode" />, - <span key=".1:0:$spanNode" />, - <a key=".2:$aNode" />, - 'string', - 1234, - ]); + expect(mappedChildren).toEqual( + gate(flags => flags.enableBigIntSupport) + ? [ + <div key=".$divNode" />, + <span key=".1:0:$spanNode" />, + <a key=".2:$aNode" />, + 'string', + 1234, + 9n, + ] + : [ + <div key=".$divNode" />, + <span key=".1:0:$spanNode" />, + <a key=".2:$aNode" />, + 'string', + 1234, + ], + ); }); it('should be called for each child in nested structure', () => { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 1655c4eed031e..0d283614d344f 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -174,6 +174,8 @@ export const disableClientCache = false; // Changes Server Components Reconciliation when they have keys export const enableServerComponentKeys = __NEXT_MAJOR__; +export const enableBigIntSupport = __NEXT_MAJOR__; + /** * Enables a new error detection for infinite render loops from updates caused * by setState or similar outside of the component owning the state. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 1bf08bdffee0a..83f004e7ca834 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -104,5 +104,7 @@ export const enableRefAsProp = false; export const enableReactTestRendererWarning = false; +export const enableBigIntSupport = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 703c1b706c113..33226be203439 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -95,5 +95,7 @@ export const enableRefAsProp = false; export const enableReactTestRendererWarning = false; +export const enableBigIntSupport = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 7a76a71b2750a..0bc3beb753d89 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -99,6 +99,7 @@ export const enableInfiniteRenderLoopDetection = false; const __NEXT_MAJOR__ = __EXPERIMENTAL__; export const enableRefAsProp = __NEXT_MAJOR__; export const enableReactTestRendererWarning = false; +export const enableBigIntSupport = __NEXT_MAJOR__; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 166fcd587111e..c9a7cece9cca9 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -91,5 +91,7 @@ export const enableRefAsProp = false; export const enableReactTestRendererWarning = false; +export const enableBigIntSupport = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 1728537564267..034c8525cdc07 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -94,5 +94,7 @@ export const enableRefAsProp = false; export const enableReactTestRendererWarning = false; +export const enableBigIntSupport = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 1074b75463068..65b0a891aaaea 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -119,5 +119,7 @@ export const enableServerComponentLogs = true; export const enableReactTestRendererWarning = false; +export const enableBigIntSupport = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType);