Add support for rendering BigInt (#24580)
Sebastian Silbermann committed
Feb 26, 2024 at 19:18 UTC
2f240c91ed54900adee213565cb2039e161629e9
30 files changed
+389
-36
packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js
+5
-1
@@ -58,5 +58,9 @@ export default function UnserializableProps(): React.Node {
58
}
59
60
function ChildComponent(props: any) {
61
- return null;
61
+ return (
62
+ <>
63
+ <div>{props.bigInt}</div>
64
+ </>
65
+ );
66
}
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+19
-4
@@ -66,6 +66,7 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO
66
import sanitizeURL from '../shared/sanitizeURL';
67
68
import {
69
+ enableBigIntSupport,
70
enableCustomElementPropertySupport,
71
enableClientRenderFallbackOnTextMismatch,
72
enableFormActions,
@@ -326,7 +327,7 @@ function normalizeMarkupForTextOrAttribute(markup: mixed): string {
327
328
export function checkForUnmatchedText(
329
serverText: string,
329
- clientText: string | number,
330
+ clientText: string | number | bigint,
331
isConcurrentMode: boolean,
332
shouldWarnDev: boolean,
333
) {
@@ -397,12 +398,17 @@ function setProp(
398
if (canSetTextContent) {
399
setTextContent(domElement, value);
400
}
400
- } else if (typeof value === 'number') {
401
+ } else if (
402
+ typeof value === 'number' ||
403
+ (enableBigIntSupport && typeof value === 'bigint')
404
+ ) {
405
if (__DEV__) {
406
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
407
validateTextNesting('' + value, tag);
408
}
409
const canSetTextContent = tag !== 'body';
410
if (canSetTextContent) {
411
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
412
setTextContent(domElement, '' + value);
413
}
414
}
@@ -955,7 +961,11 @@ function setPropOnCustomElement(
961
case 'children': {
962
if (typeof value === 'string') {
963
setTextContent(domElement, value);
958
- } else if (typeof value === 'number') {
964
+ } else if (
965
+ typeof value === 'number' ||
966
+ (enableBigIntSupport && typeof value === 'bigint')
967
+ ) {
968
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
969
setTextContent(domElement, '' + value);
970
}
971
break;
@@ -2817,7 +2827,12 @@ export function diffHydratedProperties(
2827
// even listeners these nodes might be wired up to.
2828
// TODO: Warn if there is more than a single textNode as a child.
2829
// TODO: Should we use domElement.firstChild.nodeValue to compare?
2820
- if (typeof children === 'string' || typeof children === 'number') {
2830
+ if (
2831
+ typeof children === 'string' ||
2832
+ typeof children === 'number' ||
2833
+ (enableBigIntSupport && typeof children === 'bigint')
2834
+ ) {
2835
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
2836
if (domElement.textContent !== '' + children) {
2837
if (props.suppressHydrationWarning !== true) {
2838
checkForUnmatchedText(
packages/react-dom-bindings/src/client/ReactDOMOption.js
+6
-1
@@ -8,6 +8,7 @@
8
*/
9
10
import {Children} from 'react';
11
+import {enableBigIntSupport} from 'shared/ReactFeatureFlags';
12
13
let didWarnSelectedSetOnOption = false;
14
let didWarnInvalidChild = false;
@@ -26,7 +27,11 @@ export function validateOptionProps(element: Element, props: Object) {
27
if (child == null) {
28
return;
29
}
29
- if (typeof child === 'string' || typeof child === 'number') {
30
+ if (
31
+ typeof child === 'string' ||
32
+ typeof child === 'number' ||
33
+ (enableBigIntSupport && typeof child === 'bigint')
34
+ ) {
35
return;
36
}
37
if (!didWarnInvalidChild) {
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+2
@@ -89,6 +89,7 @@ import {
89
import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
90
91
import {
92
+ enableBigIntSupport,
93
enableCreateEventHandleAPI,
94
enableScopeAPI,
95
enableFloat,
@@ -548,6 +549,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean {
549
type === 'noscript' ||
550
typeof props.children === 'string' ||
551
typeof props.children === 'number' ||
552
+ (enableBigIntSupport && typeof props.children === 'bigint') ||
553
(typeof props.dangerouslySetInnerHTML === 'object' &&
554
props.dangerouslySetInnerHTML !== null &&
555
props.dangerouslySetInnerHTML.__html != null)
packages/react-dom-bindings/src/client/ToStringValue.js
+8
@@ -8,10 +8,12 @@
8
*/
9
10
import {checkFormFieldValueStringCoercion} from 'shared/CheckStringCoercion';
11
+import {enableBigIntSupport} from 'shared/ReactFeatureFlags';
12
13
export opaque type ToStringValue =
14
| boolean
15
| number
16
+ | bigint
17
| Object
18
| string
19
| null
@@ -28,6 +30,12 @@ export function toString(value: ToStringValue): string {
30
31
export function getToStringValue(value: mixed): ToStringValue {
32
switch (typeof value) {
33
+ case 'bigint':
34
+ if (!enableBigIntSupport) {
35
+ // bigint is assigned as empty string
36
+ return '';
37
+ }
38
+ // fallthrough for BigInt support
39
case 'boolean':
40
case 'number':
41
case 'string':
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+17
-7
@@ -28,6 +28,7 @@ import {
28
import {Children} from 'react';
29
30
import {
31
+ enableBigIntSupport,
32
enableFilterEmptyStringAttributesDOM,
33
enableCustomElementPropertySupport,
34
enableFloat,
@@ -1626,7 +1627,9 @@ function flattenOptionChildren(children: mixed): string {
1627
if (
1628
!didWarnInvalidOptionChildren &&
1629
typeof child !== 'string' &&
1629
- typeof child !== 'number'
1630
+ typeof child !== 'number' &&
1631
+ ((enableBigIntSupport && typeof child !== 'bigint') ||
1632
+ !enableBigIntSupport)
1633
) {
1634
didWarnInvalidOptionChildren = true;
1635
console.error(
@@ -2960,36 +2963,40 @@ function pushTitle(
2963
2964
if (Array.isArray(children) && children.length > 1) {
2965
console.error(
2963
- '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.' +
2966
+ '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.' +
2967
' 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' +
2968
' 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.' +
2969
' For example: <title>hello {nameOfUser}</title>. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop' +
2970
' is using this form try rewriting it using a template string: <title>{`hello ${nameOfUser}`}</title>.',
2971
+ enableBigIntSupport ? ', bigint' : '',
2972
children.length,
2973
);
2974
} else if (typeof child === 'function' || typeof child === 'symbol') {
2975
const childType =
2976
typeof child === 'function' ? 'a Function' : 'a Sybmol';
2977
console.error(
2974
- 'React expect children of <title> tags to be a string, number, or object with a novel `toString` method but found %s instead.' +
2978
+ 'React expect children of <title> tags to be a string, number%s, or object with a novel `toString` method but found %s instead.' +
2979
' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title>' +
2980
' tags to a single string value.',
2981
+ enableBigIntSupport ? ', bigint' : '',
2982
childType,
2983
);
2984
} else if (child && child.toString === {}.toString) {
2985
if (child.$$typeof != null) {
2986
console.error(
2982
- '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' +
2987
+ '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' +
2988
' 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' +
2989
' 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' +
2990
' 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.',
2991
+ enableBigIntSupport ? ', bigint' : '',
2992
);
2993
} else {
2994
console.error(
2989
- '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' +
2995
+ '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' +
2996
' 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' +
2997
' 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>' +
2998
' 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>.',
2999
+ enableBigIntSupport ? ', bigint' : '',
3000
);
3001
}
3002
}
@@ -3123,14 +3130,17 @@ function pushStartTitle(
3130
} else if (
3131
childForValidation != null &&
3132
typeof childForValidation !== 'string' &&
3126
- typeof childForValidation !== 'number'
3133
+ typeof childForValidation !== 'number' &&
3134
+ ((enableBigIntSupport && typeof childForValidation !== 'bigint') ||
3135
+ !enableBigIntSupport)
3136
) {
3137
console.error(
3129
- 'A title element received a value that was not a string or number for children. ' +
3138
+ 'A title element received a value that was not a string or number%s for children. ' +
3139
'In the browser title Elements can only have Text Nodes as children. If ' +
3140
'the children being rendered output more than a single text node in aggregate the browser ' +
3141
'will display markup and comments as text in the title and hydration will likely fail and ' +
3142
'fall back to client rendering',
3143
+ enableBigIntSupport ? ' or bigint' : '',
3144
);
3145
}
3146
}
packages/react-dom-bindings/src/server/escapeTextForBrowser.js
+6
-1
@@ -39,6 +39,7 @@
39
*/
40
41
import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
42
+import {enableBigIntSupport} from 'shared/ReactFeatureFlags';
43
44
const matchHtmlRegExp = /["'&<>]/;
45
@@ -106,7 +107,11 @@ function escapeHtml(string: string) {
107
* @return {string} An escaped string.
108
*/
109
function escapeTextForBrowser(text: string | number | boolean): string {
109
- if (typeof text === 'boolean' || typeof text === 'number') {
110
+ if (
111
+ typeof text === 'boolean' ||
112
+ typeof text === 'number' ||
113
+ (enableBigIntSupport && typeof text === 'bigint')
114
+ ) {
115
// this shortcircuit helps perf for types that we know will never have
116
// special characters, especially given that this function is used often
117
// for numeric dom ids.
packages/react-dom/src/__tests__/ReactDOMFiber-test.js
+11
@@ -60,6 +60,17 @@ describe('ReactDOMFiber', () => {
60
expect(container.textContent).toEqual('10');
61
});
62
63
+ // @gate enableBigIntSupport
64
+ it('should render bigints as children', async () => {
65
+ const Box = ({value}) => <div>{value}</div>;
66
+
67
+ await act(async () => {
68
+ root.render(<Box value={10n} />);
69
+ });
70
+
71
+ expect(container.textContent).toEqual('10');
72
+ });
73
+
74
it('should call an effect after mount/update (replacing render callback pattern)', async () => {
75
function Component() {
76
React.useEffect(() => {
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+129
-2
@@ -3373,6 +3373,17 @@ describe('ReactDOMFizzServer', () => {
3373
);
3374
});
3375
3376
+ // @gate enableBigIntSupport
3377
+ it('Supports bigint', async () => {
3378
+ await act(async () => {
3379
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3380
+ <div>{10n}</div>,
3381
+ );
3382
+ pipe(writable);
3383
+ });
3384
+ expect(getVisibleChildren(container)).toEqual(<div>10</div>);
3385
+ });
3386
+
3387
it('Supports custom abort reasons with a string', async () => {
3388
function App() {
3389
return (
@@ -5642,6 +5653,60 @@ describe('ReactDOMFizzServer', () => {
5653
expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5654
});
5655
5656
+ it('should accept a single number child', async () => {
5657
+ // a Single number child
5658
+ function App() {
5659
+ return (
5660
+ <head>
5661
+ <title>4</title>
5662
+ </head>
5663
+ );
5664
+ }
5665
+
5666
+ await act(() => {
5667
+ const {pipe} = renderToPipeableStream(<App />);
5668
+ pipe(writable);
5669
+ });
5670
+ expect(getVisibleChildren(document.head)).toEqual(<title>4</title>);
5671
+
5672
+ const errors = [];
5673
+ ReactDOMClient.hydrateRoot(container, <App />, {
5674
+ onRecoverableError(error) {
5675
+ errors.push(error.message);
5676
+ },
5677
+ });
5678
+ await waitForAll([]);
5679
+ expect(errors).toEqual([]);
5680
+ expect(getVisibleChildren(document.head)).toEqual(<title>4</title>);
5681
+ });
5682
+
5683
+ it('should accept a single bigint child', async () => {
5684
+ // a Single number child
5685
+ function App() {
5686
+ return (
5687
+ <head>
5688
+ <title>5n</title>
5689
+ </head>
5690
+ );
5691
+ }
5692
+
5693
+ await act(() => {
5694
+ const {pipe} = renderToPipeableStream(<App />);
5695
+ pipe(writable);
5696
+ });
5697
+ expect(getVisibleChildren(document.head)).toEqual(<title>5n</title>);
5698
+
5699
+ const errors = [];
5700
+ ReactDOMClient.hydrateRoot(container, <App />, {
5701
+ onRecoverableError(error) {
5702
+ errors.push(error.message);
5703
+ },
5704
+ });
5705
+ await waitForAll([]);
5706
+ expect(errors).toEqual([]);
5707
+ expect(getVisibleChildren(document.head)).toEqual(<title>5n</title>);
5708
+ });
5709
+
5710
it('should accept children array of length 1 containing a string', async () => {
5711
// a Single string child
5712
function App() {
@@ -5684,7 +5749,9 @@ describe('ReactDOMFizzServer', () => {
5749
pipe(writable);
5750
});
5751
}).toErrorDev([
5687
- '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>.',
5752
+ 'React expects the `children` prop of <title> tags to be a string, number' +
5753
+ gate(flags => (flags.enableBigIntSupport ? ', bigint' : '')) +
5754
+ ', 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>.',
5755
]);
5756
5757
if (gate(flags => flags.enableFloat)) {
@@ -5744,7 +5811,67 @@ describe('ReactDOMFizzServer', () => {
5811
pipe(writable);
5812
});
5813
}).toErrorDev([
5747
- '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.',
5814
+ 'React expects the `children` prop of <title> tags to be a string, number' +
5815
+ gate(flags => (flags.enableBigIntSupport ? ', bigint' : '')) +
5816
+ ', 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.',
5817
+ ]);
5818
+ } else {
5819
+ await expect(async () => {
5820
+ await act(() => {
5821
+ const {pipe} = renderToPipeableStream(<App />);
5822
+ pipe(writable);
5823
+ });
5824
+ }).toErrorDev([
5825
+ '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',
5826
+ ]);
5827
+ }
5828
+
5829
+ if (gate(flags => flags.enableFloat)) {
5830
+ // object titles are toStringed when float is on
5831
+ expect(getVisibleChildren(document.head)).toEqual(
5832
+ <title>{'[object Object]'}</title>,
5833
+ );
5834
+ } else {
5835
+ expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5836
+ }
5837
+
5838
+ const errors = [];
5839
+ ReactDOMClient.hydrateRoot(document.head, <App />, {
5840
+ onRecoverableError(error) {
5841
+ errors.push(error.message);
5842
+ },
5843
+ });
5844
+ await waitForAll([]);
5845
+ expect(errors).toEqual([]);
5846
+ if (gate(flags => flags.enableFloat)) {
5847
+ // object titles are toStringed when float is on
5848
+ expect(getVisibleChildren(document.head)).toEqual(
5849
+ <title>{'[object Object]'}</title>,
5850
+ );
5851
+ } else {
5852
+ expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5853
+ }
5854
+ });
5855
+
5856
+ it('should warn in dev if you pass an object that does not implement toString as a child to <title>', async () => {
5857
+ function App() {
5858
+ return (
5859
+ <head>
5860
+ <title>{{}}</title>
5861
+ </head>
5862
+ );
5863
+ }
5864
+
5865
+ if (gate(flags => flags.enableFloat)) {
5866
+ await expect(async () => {
5867
+ await act(() => {
5868
+ const {pipe} = renderToPipeableStream(<App />);
5869
+ pipe(writable);
5870
+ });
5871
+ }).toErrorDev([
5872
+ 'React expects the `children` prop of <title> tags to be a string, number' +
5873
+ gate(flags => (flags.enableBigIntSupport ? ', bigint' : '')) +
5874
+ ', 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>.',
5875
]);
5876
} else {
5877
await expect(async () => {
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+29
@@ -657,6 +657,16 @@ describe('ReactDOMInput', () => {
657
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
658
});
659
660
+ it('should render bigint defaultValue for SSR', () => {
661
+ const markup = ReactDOMServer.renderToString(
662
+ <input type="text" defaultValue={5n} />,
663
+ );
664
+ const div = document.createElement('div');
665
+ div.innerHTML = markup;
666
+ expect(div.firstChild.getAttribute('value')).toBe('5');
667
+ expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
668
+ });
669
+
670
it('should render value for SSR', () => {
671
const element = <input type="text" value="1" onChange={() => {}} />;
672
const markup = ReactDOMServer.renderToString(element);
@@ -666,6 +676,15 @@ describe('ReactDOMInput', () => {
676
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
677
});
678
679
+ it('should render bigint value for SSR', () => {
680
+ const element = <input type="text" value={5n} onChange={() => {}} />;
681
+ const markup = ReactDOMServer.renderToString(element);
682
+ const div = document.createElement('div');
683
+ div.innerHTML = markup;
684
+ expect(div.firstChild.getAttribute('value')).toBe('5');
685
+ expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
686
+ });
687
+
688
it('should render name attribute if it is supplied', async () => {
689
await act(() => {
690
root.render(<input type="text" name="name" />);
@@ -830,6 +849,16 @@ describe('ReactDOMInput', () => {
849
expect(node.value).toBe('0');
850
});
851
852
+ // @gate enableBigIntSupport
853
+ it('should display `value` of bigint 5', async () => {
854
+ await act(() => {
855
+ root.render(<input type="text" value={5n} onChange={emptyFunction} />);
856
+ });
857
+ const node = container.firstChild;
858
+
859
+ expect(node.value).toBe('5');
860
+ });
861
+
862
it('should allow setting `value` to `true`', async () => {
863
await act(() => {
864
root.render(<input type="text" value="yolo" onChange={emptyFunction} />);
packages/react-dom/src/__tests__/ReactDOMOption-test.js
+7
@@ -172,6 +172,13 @@ describe('ReactDOMOption', () => {
172
expect(node.value).toBe('hello');
173
});
174
175
+ // @gate enableBigIntSupport
176
+ it('should support bigint values', () => {
177
+ const node = ReactTestUtils.renderIntoDocument(<option>{5n}</option>);
178
+ expect(node.innerHTML).toBe('5');
179
+ expect(node.value).toBe('5');
180
+ });
181
+
182
it('should be able to use dangerouslySetInnerHTML on option', () => {
183
const stub = <option dangerouslySetInnerHTML={{__html: 'foobar'}} />;
184
let node;
packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js
+10
@@ -72,6 +72,16 @@ describe('ReactDOMServerIntegration', () => {
72
expect(e.nodeValue).toMatch('42');
73
});
74
75
+ itRenders('a bigint', async render => {
76
+ const e = await render(42n);
77
+ if (gate(flags => flags.enableBigIntSupport)) {
78
+ expect(e.nodeType).toBe(3);
79
+ expect(e.nodeValue).toMatch('42');
80
+ } else {
81
+ expect(e).toBe(null);
82
+ }
83
+ });
84
+
85
itRenders('an array with one child', async render => {
86
const e = await render([<div key={1}>text1</div>]);
87
const parent = e.parentNode;
packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js
+14
-1
@@ -32,7 +32,8 @@ function initModules() {
32
};
33
}
34
35
-const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
35
+const {resetModules, itRenders, serverRender, streamRender} =
36
+ ReactDOMServerIntegrationUtils(initModules);
37
38
// TODO: Run this in React Fire mode after we figure out the SSR behavior.
39
const desc = disableInputAttributeSyncing ? xdescribe : describe;
@@ -46,6 +47,18 @@ desc('ReactDOMServerIntegrationInput', () => {
47
expect(e.value).toBe('foo');
48
});
49
50
+ itRenders('an input with a bigint value and an onChange', async render => {
51
+ console.log(gate(flags => flags.enableBigIntSupport));
52
+ const e = await render(<input value={5n} onChange={() => {}} />);
53
+ expect(e.value).toBe(
54
+ gate(flags => flags.enableBigIntSupport) ||
55
+ render === serverRender ||
56
+ render === streamRender
57
+ ? '5'
58
+ : '',
59
+ );
60
+ });
61
+
62
itRenders('an input with a value and readOnly', async render => {
63
const e = await render(<input value="foo" readOnly={true} />);
64
expect(e.value).toBe('foo');
packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js
+6
-2
@@ -218,11 +218,15 @@ describe('ReactDOMServerIntegrationSelect', () => {
218
itRenders('a select option with flattened children', async render => {
219
const e = await render(
220
<select value="bar" readOnly={true}>
221
- <option value="bar">A {'B'}</option>
221
+ <option value="bar">
222
+ A {'B'} {5n}
223
+ </option>
224
</select>,
225
);
226
const option = e.options[0];
225
- expect(option.textContent).toBe('A B');
227
+ expect(option.textContent).toBe(
228
+ gate(flags => flags.enableBigIntSupport) ? 'A B 5' : 'A B ',
229
+ );
230
expect(option.value).toBe('bar');
231
expect(option.selected).toBe(true);
232
});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js
+14
-2
@@ -30,7 +30,8 @@ function initModules() {
30
};
31
}
32
33
-const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
33
+const {resetModules, itRenders, serverRender, streamRender} =
34
+ ReactDOMServerIntegrationUtils(initModules);
35
36
describe('ReactDOMServerIntegrationTextarea', () => {
37
beforeEach(() => {
@@ -47,12 +48,23 @@ describe('ReactDOMServerIntegrationTextarea', () => {
48
expect(e.value).toBe('foo');
49
});
50
51
+ itRenders('a textarea with a bigint value and an onChange', async render => {
52
+ const e = await render(<textarea value={5n} onChange={() => {}} />);
53
+ expect(e.getAttribute('value')).toBe(null);
54
+ expect(e.value).toBe(
55
+ gate(flags => flags.enableBigIntSupport) ||
56
+ render === serverRender ||
57
+ render === streamRender
58
+ ? '5'
59
+ : '',
60
+ );
61
+ });
62
+
63
itRenders('a textarea with a value of undefined', async render => {
64
const e = await render(<textarea value={undefined} />);
65
expect(e.getAttribute('value')).toBe(null);
66
expect(e.value).toBe('');
67
});
55
-
68
itRenders('a textarea with a value and readOnly', async render => {
69
const e = await render(<textarea value="foo" readOnly={true} />);
70
// textarea DOM elements don't have a value **attribute**, the text is
packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
+13
@@ -80,6 +80,19 @@ describe('ReactDOMTextarea', () => {
80
expect(node.value).toBe('0');
81
});
82
83
+ // @gate enableBigIntSupport
84
+ it('should display `defaultValue` of bigint 0', async () => {
85
+ const container = document.createElement('div');
86
+ const root = ReactDOMClient.createRoot(container);
87
+ const node = await renderTextarea(
88
+ <textarea defaultValue={0n} />,
89
+ container,
90
+ root,
91
+ );
92
+
93
+ expect(node.value).toBe('0');
94
+ });
95
+
96
it('should display "false" for `defaultValue` of `false`', async () => {
97
const container = document.createElement('div');
98
const root = ReactDOMClient.createRoot(container);
packages/react-dom/src/__tests__/ReactMultiChildText-test.js
+10
@@ -58,6 +58,7 @@ const expectChildren = function (container, children) {
58
continue;
59
}
60
textNode = outerNode.childNodes[mountIndex];
61
+ expect(textNode != null).toBe(true);
62
expect(textNode.nodeType).toBe(3);
63
expect(textNode.data).toBe(child);
64
mountIndex++;
@@ -173,6 +174,15 @@ describe('ReactMultiChildText', () => {
174
]);
175
});
176
177
+ // @gate enableBigIntSupport
178
+ it('should correctly handle bigint children for render and update', async () => {
179
+ // prettier-ignore
180
+ await testAllPermutations([
181
+ 10n, '10',
182
+ [10n], ['10']
183
+ ]);
184
+ });
185
+
186
it('should throw if rendering both HTML and children', async () => {
187
const container = document.createElement('div');
188
const root = ReactDOMClient.createRoot(container);
packages/react-noop-renderer/src/createReactNoop.js
+11
-2
@@ -272,7 +272,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
272
throw new Error('Error in host config.');
273
}
274
return (
275
- typeof props.children === 'string' || typeof props.children === 'number'
275
+ typeof props.children === 'string' ||
276
+ typeof props.children === 'number' ||
277
+ typeof props.children === 'bigint'
278
);
279
}
280
@@ -828,7 +830,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
830
return childToJSX(child[0], null);
831
}
832
const children = child.map(c => childToJSX(c, null));
831
- if (children.every(c => typeof c === 'string' || typeof c === 'number')) {
833
+ if (
834
+ children.every(
835
+ c =>
836
+ typeof c === 'string' ||
837
+ typeof c === 'number' ||
838
+ typeof c === 'bigint',
839
+ )
840
+ ) {
841
return children.join('');
842
}
843
return children;
packages/react-reconciler/src/ReactChildFiber.js
+13
-4
@@ -25,6 +25,7 @@ import {
25
Forked,
26
PlacementDEV,
27
} from './ReactFiberFlags';
28
+import {enableBigIntSupport} from 'shared/ReactFeatureFlags';
29
import {
30
getIteratorFn,
31
REACT_ELEMENT_TYPE,
@@ -665,12 +666,14 @@ function createChildReconciler(
666
): Fiber | null {
667
if (
668
(typeof newChild === 'string' && newChild !== '') ||
668
- typeof newChild === 'number'
669
+ typeof newChild === 'number' ||
670
+ (enableBigIntSupport && typeof newChild === 'bigint')
671
) {
672
// Text nodes don't have keys. If the previous node is implicitly keyed
673
// we can continue to replace it without aborting even if it is not a text
674
// node.
675
const created = createFiberFromText(
676
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
677
'' + newChild,
678
returnFiber.mode,
679
lanes,
@@ -785,7 +788,8 @@ function createChildReconciler(
788
789
if (
790
(typeof newChild === 'string' && newChild !== '') ||
788
- typeof newChild === 'number'
791
+ typeof newChild === 'number' ||
792
+ (enableBigIntSupport && typeof newChild === 'bigint')
793
) {
794
// Text nodes don't have keys. If the previous node is implicitly keyed
795
// we can continue to replace it without aborting even if it is not a text
@@ -796,6 +800,7 @@ function createChildReconciler(
800
return updateTextNode(
801
returnFiber,
802
oldFiber,
803
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
804
'' + newChild,
805
lanes,
806
debugInfo,
@@ -908,7 +913,8 @@ function createChildReconciler(
913
): Fiber | null {
914
if (
915
(typeof newChild === 'string' && newChild !== '') ||
911
- typeof newChild === 'number'
916
+ typeof newChild === 'number' ||
917
+ (enableBigIntSupport && typeof newChild === 'bigint')
918
) {
919
// Text nodes don't have keys, so we neither have to check the old nor
920
// new node for the key. If both are text nodes, they match.
@@ -916,6 +922,7 @@ function createChildReconciler(
922
return updateTextNode(
923
returnFiber,
924
matchedFiber,
925
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
926
'' + newChild,
927
lanes,
928
debugInfo,
@@ -1723,12 +1730,14 @@ function createChildReconciler(
1730
1731
if (
1732
(typeof newChild === 'string' && newChild !== '') ||
1726
- typeof newChild === 'number'
1733
+ typeof newChild === 'number' ||
1734
+ (enableBigIntSupport && typeof newChild === 'bigint')
1735
) {
1736
return placeSingleChild(
1737
reconcileSingleTextNode(
1738
returnFiber,
1739
currentFirstChild,
1740
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
1741
'' + newChild,
1742
lanes,
1743
),
packages/react-reconciler/src/__tests__/ReactTopLevelText-test.js
+8
@@ -39,4 +39,12 @@ describe('ReactTopLevelText', () => {
39
await waitForAll([]);
40
expect(ReactNoop).toMatchRenderedOutput('10');
41
});
42
+
43
+ // @gate enableBigIntSupport
44
+ it('should render a component returning bigints directly from render', async () => {
45
+ const Text = ({value}) => value;
46
+ ReactNoop.render(<Text value={10n} />);
47
+ await waitForAll([]);
48
+ expect(ReactNoop).toMatchRenderedOutput('10');
49
+ });
50
});
packages/react-server/src/ReactFizzServer.js
+5
-1
@@ -139,6 +139,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
139
import {
140
disableLegacyContext,
141
disableModulePatternComponents,
142
+ enableBigIntSupport,
143
enableScopeAPI,
144
enableSuspenseAvoidThisFallbackFizz,
145
enableFloat,
@@ -2364,7 +2365,10 @@ function renderNodeDestructive(
2365
return;
2366
}
2367
2367
- if (typeof node === 'number') {
2368
+ if (
2369
+ typeof node === 'number' ||
2370
+ (enableBigIntSupport && typeof node === 'bigint')
2371
+ ) {
2372
const segment = task.blockedSegment;
2373
if (segment === null) {
2374
// We assume a text node doesn't have a representation in the replay set,
packages/react/src/ReactChildren.js
+6
@@ -16,6 +16,7 @@ import type {
16
} from 'shared/ReactTypes';
17
18
import isArray from 'shared/isArray';
19
+import {enableBigIntSupport} from 'shared/ReactFeatureFlags';
20
import {
21
getIteratorFn,
22
REACT_ELEMENT_TYPE,
@@ -163,6 +164,11 @@ function mapIntoArray(
164
invokeCallback = true;
165
} else {
166
switch (type) {
167
+ case 'bigint':
168
+ if (!enableBigIntSupport) {
169
+ break;
170
+ }
171
+ // fallthrough for enabled BigInt support
172
case 'string':
173
case 'number':
174
invokeCallback = true;
packages/react/src/__tests__/ReactChildren-test.js
+27
-8
@@ -183,11 +183,14 @@ describe('ReactChildren', () => {
183
{false}
184
{null}
185
{undefined}
186
+ {9n}
187
</div>
188
);
189
190
function assertCalls() {
190
- expect(callback).toHaveBeenCalledTimes(9);
191
+ expect(callback).toHaveBeenCalledTimes(
192
+ gate(flags => flags.enableBigIntSupport) ? 10 : 9,
193
+ );
194
expect(callback).toHaveBeenCalledWith(div, 0);
195
expect(callback).toHaveBeenCalledWith(span, 1);
196
expect(callback).toHaveBeenCalledWith(a, 2);
@@ -197,6 +200,11 @@ describe('ReactChildren', () => {
200
expect(callback).toHaveBeenCalledWith(null, 6);
201
expect(callback).toHaveBeenCalledWith(null, 7);
202
expect(callback).toHaveBeenCalledWith(null, 8);
203
+ if (gate(flags => flags.enableBigIntSupport)) {
204
+ expect(callback).toHaveBeenCalledWith(9n, 9);
205
+ } else {
206
+ expect(callback).not.toHaveBeenCalledWith(9n, 9);
207
+ }
208
callback.mockClear();
209
}
210
@@ -209,13 +217,24 @@ describe('ReactChildren', () => {
217
context,
218
);
219
assertCalls();
212
- expect(mappedChildren).toEqual([
213
- <div key=".$divNode" />,
214
- <span key=".1:0:$spanNode" />,
215
- <a key=".2:$aNode" />,
216
- 'string',
217
- 1234,
218
- ]);
220
+ expect(mappedChildren).toEqual(
221
+ gate(flags => flags.enableBigIntSupport)
222
+ ? [
223
+ <div key=".$divNode" />,
224
+ <span key=".1:0:$spanNode" />,
225
+ <a key=".2:$aNode" />,
226
+ 'string',
227
+ 1234,
228
+ 9n,
229
+ ]
230
+ : [
231
+ <div key=".$divNode" />,
232
+ <span key=".1:0:$spanNode" />,
233
+ <a key=".2:$aNode" />,
234
+ 'string',
235
+ 1234,
236
+ ],
237
+ );
238
});
239
240
it('should be called for each child in nested structure', () => {
packages/shared/ReactFeatureFlags.js
+2
@@ -174,6 +174,8 @@ export const disableClientCache = false;
174
// Changes Server Components Reconciliation when they have keys
175
export const enableServerComponentKeys = __NEXT_MAJOR__;
176
177
+export const enableBigIntSupport = __NEXT_MAJOR__;
178
+
179
/**
180
* Enables a new error detection for infinite render loops from updates caused
181
* by setState or similar outside of the component owning the state.
packages/shared/forks/ReactFeatureFlags.native-fb.js
+2
@@ -104,5 +104,7 @@ export const enableRefAsProp = false;
104
105
export const enableReactTestRendererWarning = false;
106
107
+export const enableBigIntSupport = false;
108
+
109
// Flow magic to verify the exports of this file match the original version.
110
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -95,5 +95,7 @@ export const enableRefAsProp = false;
95
96
export const enableReactTestRendererWarning = false;
97
98
+export const enableBigIntSupport = false;
99
+
100
// Flow magic to verify the exports of this file match the original version.
101
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -99,6 +99,7 @@ export const enableInfiniteRenderLoopDetection = false;
99
const __NEXT_MAJOR__ = __EXPERIMENTAL__;
100
export const enableRefAsProp = __NEXT_MAJOR__;
101
export const enableReactTestRendererWarning = false;
102
+export const enableBigIntSupport = __NEXT_MAJOR__;
103
104
// Flow magic to verify the exports of this file match the original version.
105
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+2
@@ -91,5 +91,7 @@ export const enableRefAsProp = false;
91
92
export const enableReactTestRendererWarning = false;
93
94
+export const enableBigIntSupport = false;
95
+
96
// Flow magic to verify the exports of this file match the original version.
97
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -94,5 +94,7 @@ export const enableRefAsProp = false;
94
95
export const enableReactTestRendererWarning = false;
96
97
+export const enableBigIntSupport = false;
98
+
99
// Flow magic to verify the exports of this file match the original version.
100
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -119,5 +119,7 @@ export const enableServerComponentLogs = true;
119
120
export const enableReactTestRendererWarning = false;
121
122
+export const enableBigIntSupport = false;
123
+
124
// Flow magic to verify the exports of this file match the original version.
125
((((null: any): ExportsType): FeatureFlagsType): ExportsType);