Add support for rendering BigInt (#24580)

This commit is contained in:
Sebastian Silbermann
2024-02-26 19:18:50 +01:00
committed by GitHub
parent a27fe22548
commit ce5eb600d0
30 changed files with 389 additions and 36 deletions
@@ -58,5 +58,9 @@ export default function UnserializableProps(): React.Node {
}
function ChildComponent(props: any) {
return null;
return (
<>
<div>{props.bigInt}</div>
</>
);
}
+19 -4
View File
@@ -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(
+6 -1
View File
@@ -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) {
@@ -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)
@@ -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':
@@ -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 <title> 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}</title>. 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: <title>{`hello ${nameOfUser}`}</title>.',
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 <title> 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' : '',
);
}
}
@@ -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.
+11
View File
@@ -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(() => {
+129 -2
View File
@@ -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</title>);
});
it('should accept a single number child', async () => {
// a Single number child
function App() {
return (
<head>
<title>4</title>
</head>
);
}
await act(() => {
const {pipe} = renderToPipeableStream(<App />);
pipe(writable);
});
expect(getVisibleChildren(document.head)).toEqual(<title>4</title>);
const errors = [];
ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
errors.push(error.message);
},
});
await waitForAll([]);
expect(errors).toEqual([]);
expect(getVisibleChildren(document.head)).toEqual(<title>4</title>);
});
it('should accept a single bigint child', async () => {
// a Single number child
function App() {
return (
<head>
<title>5n</title>
</head>
);
}
await act(() => {
const {pipe} = renderToPipeableStream(<App />);
pipe(writable);
});
expect(getVisibleChildren(document.head)).toEqual(<title>5n</title>);
const errors = [];
ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
errors.push(error.message);
},
});
await waitForAll([]);
expect(errors).toEqual([]);
expect(getVisibleChildren(document.head)).toEqual(<title>5n</title>);
});
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 <title> 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}</title>. 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: <title>{`hello ${nameOfUser}`}</title>.',
'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 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}</title>. 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: <title>{`hello ${nameOfUser}`}</title>.',
]);
if (gate(flags => flags.enableFloat)) {
@@ -5744,7 +5811,67 @@ describe('ReactDOMFizzServer', () => {
pipe(writable);
});
}).toErrorDev([
'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 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]'}</title>,
);
} else {
expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
}
const errors = [];
ReactDOMClient.hydrateRoot(document.head, <App />, {
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(
<title>{'[object Object]'}</title>,
);
} else {
expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
}
});
it('should warn in dev if you pass an object that does not implement toString as a child to <title>', async () => {
function App() {
return (
<head>
<title>{{}}</title>
</head>
);
}
if (gate(flags => flags.enableFloat)) {
await expect(async () => {
await act(() => {
const {pipe} = renderToPipeableStream(<App />);
pipe(writable);
});
}).toErrorDev([
'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 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 () => {
+29
View File
@@ -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} />);
@@ -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;
@@ -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;
@@ -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');
@@ -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);
});
@@ -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
@@ -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);
@@ -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);
+11 -2
View File
@@ -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;
+13 -4
View File
@@ -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,
),
@@ -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');
});
});
+5 -1
View File
@@ -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,
+6
View File
@@ -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;
@@ -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', () => {
+2
View File
@@ -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.
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);