Expose LegacyHidden type and disable <div hidden /> API in new fork (#18891)
* Expose LegacyHidden type I will use this internally at Facebook to migrate away from <div hidden />. The end goal is to migrate to the Offscreen type, but that has different semantics. This is an incremental step. * Disable <div hidden /> API in new fork Migrates to the unstable_LegacyHidden type instead. The old fork does not support the new component type, so I updated the tests to use an indirection that picks the correct API. I will remove this once the LegacyHidden (and/or Offscreen) type has landed in both implementations. * Add gated warning for `<div hidden />` API Only exists so we can detect callers in www and migrate them to the new API. Should not visible to anyone outside React Core team.
Andrew Clark committed
May 11, 2020 at 20:02 UTC
8b9c4d1688333865e702fcd65ad2ab7d83b3c33c
27 files changed
+303
-69
packages/react-dom/src/__tests__/ReactUpdates-test.js
+22
-2
@@ -25,6 +25,25 @@ describe('ReactUpdates', () => {
25
Scheduler = require('scheduler');
26
});
27
28
+ // TODO: Delete this once new API exists in both forks
29
+ function LegacyHiddenDiv({hidden, children, ...props}) {
30
+ if (gate(flags => flags.new)) {
31
+ return (
32
+ <div hidden={hidden} {...props}>
33
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
34
+ {children}
35
+ </React.unstable_LegacyHidden>
36
+ </div>
37
+ );
38
+ } else {
39
+ return (
40
+ <div hidden={hidden} {...props}>
41
+ {children}
42
+ </div>
43
+ );
44
+ }
45
+ }
46
+
47
it('should batch state when updating state twice', () => {
48
let updateCount = 0;
49
@@ -1288,6 +1307,7 @@ describe('ReactUpdates', () => {
1307
});
1308
1309
// @gate experimental
1310
+ // @gate enableLegacyHiddenType
1311
it('delays sync updates inside hidden subtrees in Concurrent Mode', () => {
1312
const container = document.createElement('div');
1313
@@ -1311,9 +1331,9 @@ describe('ReactUpdates', () => {
1331
});
1332
return (
1333
<div>
1314
- <div hidden={true}>
1334
+ <LegacyHiddenDiv hidden={true}>
1335
<Bar />
1316
- </div>
1336
+ </LegacyHiddenDiv>
1337
<Baz />
1338
</div>
1339
);
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+24
-25
@@ -75,17 +75,13 @@ import {
75
warnAboutDefaultPropsOnFunctionComponents,
76
enableScopeAPI,
77
enableBlocksAPI,
78
+ warnAboutDOMHiddenAttribute,
79
} from 'shared/ReactFeatureFlags';
80
import invariant from 'shared/invariant';
81
import shallowEqual from 'shared/shallowEqual';
82
import getComponentName from 'shared/getComponentName';
83
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
83
-import {
84
- REACT_ELEMENT_TYPE,
85
- REACT_LAZY_TYPE,
86
- REACT_LEGACY_HIDDEN_TYPE,
87
- getIteratorFn,
88
-} from 'shared/ReactSymbols';
84
+import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols';
85
import {
86
getCurrentFiberOwnerNameInDevOrNull,
87
setIsRendering,
@@ -128,7 +124,6 @@ import {
124
} from './ReactTypeOfMode';
125
import {
126
shouldSetTextContent,
131
- shouldDeprioritizeSubtree,
127
isSuspenseInstancePending,
128
isSuspenseInstanceFallback,
129
registerSuspenseInstanceRetry,
@@ -572,7 +567,15 @@ function updateOffscreenComponent(
567
current !== null ? current.memoizedState : null;
568
569
if (nextProps.mode === 'hidden') {
575
- if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
570
+ if ((workInProgress.mode & ConcurrentMode) === NoMode) {
571
+ // In legacy sync mode, don't defer the subtree. Render it now.
572
+ // TODO: Figure out what we should do in Blocking mode.
573
+ const nextState: OffscreenState = {
574
+ baseLanes: NoLanes,
575
+ };
576
+ workInProgress.memoizedState = nextState;
577
+ pushRenderLanes(workInProgress, renderLanes);
578
+ } else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
579
let nextBaseLanes;
580
if (prevState !== null) {
581
const prevBaseLanes = prevState.baseLanes;
@@ -1122,23 +1125,19 @@ function updateHostComponent(
1125
1126
markRef(current, workInProgress);
1127
1125
- if (
1126
- (workInProgress.mode & ConcurrentMode) !== NoMode &&
1127
- nextProps.hasOwnProperty('hidden')
1128
- ) {
1129
- const wrappedChildren = {
1130
- $$typeof: REACT_ELEMENT_TYPE,
1131
- type: REACT_LEGACY_HIDDEN_TYPE,
1132
- key: null,
1133
- ref: null,
1134
- props: {
1135
- children: nextChildren,
1136
- // Check the host config to see if the children are offscreen/hidden.
1137
- mode: shouldDeprioritizeSubtree(type, nextProps) ? 'hidden' : 'visible',
1138
- },
1139
- _owner: __DEV__ ? {} : null,
1140
- };
1141
- nextChildren = wrappedChildren;
1128
+ if (__DEV__) {
1129
+ if (
1130
+ warnAboutDOMHiddenAttribute &&
1131
+ (workInProgress.mode & ConcurrentMode) !== NoMode &&
1132
+ nextProps.hasOwnProperty('hidden')
1133
+ ) {
1134
+ // This warning will not be user visible. Only exists so React Core team
1135
+ // can find existing callers and migrate them to the new API.
1136
+ console.error(
1137
+ 'Detected use of DOM `hidden` attribute. Should migrate to new API. ' +
1138
+ '(owner: React Core)',
1139
+ );
1140
+ }
1141
}
1142
1143
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+16
@@ -69,6 +69,7 @@ import {
69
warnAboutDefaultPropsOnFunctionComponents,
70
enableScopeAPI,
71
enableBlocksAPI,
72
+ warnAboutDOMHiddenAttribute,
73
} from 'shared/ReactFeatureFlags';
74
import invariant from 'shared/invariant';
75
import shallowEqual from 'shared/shallowEqual';
@@ -1099,6 +1100,21 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
1100
1101
markRef(current, workInProgress);
1102
1103
+ if (__DEV__) {
1104
+ if (
1105
+ warnAboutDOMHiddenAttribute &&
1106
+ (workInProgress.mode & ConcurrentMode) !== NoMode &&
1107
+ nextProps.hasOwnProperty('hidden')
1108
+ ) {
1109
+ // This warning will not be user visible. Only exists so React Core team
1110
+ // can find existing callers and migrate them to the new API.
1111
+ console.error(
1112
+ 'Detected use of DOM `hidden` attribute. Should migrate to new API. ' +
1113
+ '(owner: React Core)',
1114
+ );
1115
+ }
1116
+ }
1117
+
1118
// Check the host config to see if the children are offscreen/hidden.
1119
if (
1120
workInProgress.mode & ConcurrentMode &&
packages/react-reconciler/src/__tests__/ReactIncremental-test.js
+32
-10
@@ -24,6 +24,25 @@ describe('ReactIncremental', () => {
24
PropTypes = require('prop-types');
25
});
26
27
+ // TODO: Delete this once new API exists in both forks
28
+ function LegacyHiddenDiv({hidden, children, ...props}) {
29
+ if (gate(flags => flags.new)) {
30
+ return (
31
+ <div hidden={hidden} {...props}>
32
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
33
+ {children}
34
+ </React.unstable_LegacyHidden>
35
+ </div>
36
+ );
37
+ } else {
38
+ return (
39
+ <div hidden={hidden} {...props}>
40
+ {children}
41
+ </div>
42
+ );
43
+ }
44
+ }
45
+
46
it('should render a simple component', () => {
47
function Bar() {
48
return <div>Hello World</div>;
@@ -210,6 +229,7 @@ describe('ReactIncremental', () => {
229
expect(inst.state).toEqual({text: 'bar', text2: 'baz'});
230
});
231
232
+ // @gate enableLegacyHiddenType
233
it('can deprioritize unfinished work and resume it later', () => {
234
function Bar(props) {
235
Scheduler.unstable_yieldValue('Bar');
@@ -226,13 +246,13 @@ describe('ReactIncremental', () => {
246
return (
247
<div>
248
<Bar>{props.text}</Bar>
229
- <section hidden={true}>
249
+ <LegacyHiddenDiv hidden={true}>
250
<Middle>{props.text}</Middle>
231
- </section>
251
+ </LegacyHiddenDiv>
252
<Bar>{props.text}</Bar>
233
- <footer hidden={true}>
253
+ <LegacyHiddenDiv hidden={true}>
254
<Middle>Footer</Middle>
235
- </footer>
255
+ </LegacyHiddenDiv>
256
</div>
257
);
258
}
@@ -255,6 +275,7 @@ describe('ReactIncremental', () => {
275
expect(Scheduler).toFlushAndYield(['Middle', 'Middle']);
276
});
277
278
+ // @gate enableLegacyHiddenType
279
it('can deprioritize a tree from without dropping work', () => {
280
function Bar(props) {
281
Scheduler.unstable_yieldValue('Bar');
@@ -271,13 +292,13 @@ describe('ReactIncremental', () => {
292
return (
293
<div>
294
<Bar>{props.text}</Bar>
274
- <section hidden={true}>
295
+ <LegacyHiddenDiv hidden={true}>
296
<Middle>{props.text}</Middle>
276
- </section>
297
+ </LegacyHiddenDiv>
298
<Bar>{props.text}</Bar>
278
- <footer hidden={true}>
299
+ <LegacyHiddenDiv hidden={true}>
300
<Middle>Footer</Middle>
280
- </footer>
301
+ </LegacyHiddenDiv>
302
</div>
303
);
304
}
@@ -1950,6 +1971,7 @@ describe('ReactIncremental', () => {
1971
});
1972
}
1973
1974
+ // @gate enableLegacyHiddenType
1975
it('provides context when reusing work', () => {
1976
class Intl extends React.Component {
1977
static childContextTypes = {
@@ -1981,12 +2003,12 @@ describe('ReactIncremental', () => {
2003
ReactNoop.render(
2004
<Intl locale="fr">
2005
<ShowLocale />
1984
- <div hidden="true">
2006
+ <LegacyHiddenDiv hidden="true">
2007
<ShowLocale />
2008
<Intl locale="ru">
2009
<ShowLocale />
2010
</Intl>
1989
- </div>
2011
+ </LegacyHiddenDiv>
2012
<ShowLocale />
2013
</Intl>,
2014
);
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+22
-2
@@ -45,6 +45,25 @@ describe('ReactIncrementalErrorHandling', () => {
45
);
46
}
47
48
+ // TODO: Delete this once new API exists in both forks
49
+ function LegacyHiddenDiv({hidden, children, ...props}) {
50
+ if (gate(flags => flags.new)) {
51
+ return (
52
+ <div hidden={hidden} {...props}>
53
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
54
+ {children}
55
+ </React.unstable_LegacyHidden>
56
+ </div>
57
+ );
58
+ } else {
59
+ return (
60
+ <div hidden={hidden} {...props}>
61
+ {children}
62
+ </div>
63
+ );
64
+ }
65
+ }
66
+
67
it('recovers from errors asynchronously', () => {
68
class ErrorBoundary extends React.Component {
69
state = {error: null};
@@ -270,6 +289,7 @@ describe('ReactIncrementalErrorHandling', () => {
289
expect(ReactNoop.getChildren()).toEqual([span('Everything is fine.')]);
290
});
291
292
+ // @gate enableLegacyHiddenType
293
it('does not include offscreen work when retrying after an error', () => {
294
function App(props) {
295
if (props.isBroken) {
@@ -280,9 +300,9 @@ describe('ReactIncrementalErrorHandling', () => {
300
return (
301
<>
302
Everything is fine
283
- <div hidden={true}>
303
+ <LegacyHiddenDiv hidden={true}>
304
<div>Offscreen content</div>
285
- </div>
305
+ </LegacyHiddenDiv>
306
</>
307
);
308
}
packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js
+34
-10
@@ -38,6 +38,25 @@ describe('ReactIncrementalSideEffects', () => {
38
return {text: t, hidden: false};
39
}
40
41
+ // TODO: Delete this once new API exists in both forks
42
+ function LegacyHiddenDiv({hidden, children, ...props}) {
43
+ if (gate(flags => flags.new)) {
44
+ return (
45
+ <div hidden={hidden} {...props}>
46
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
47
+ {children}
48
+ </React.unstable_LegacyHidden>
49
+ </div>
50
+ );
51
+ } else {
52
+ return (
53
+ <div hidden={hidden} {...props}>
54
+ {children}
55
+ </div>
56
+ );
57
+ }
58
+ }
59
+
60
it('can update child nodes of a host instance', () => {
61
function Bar(props) {
62
return <span>{props.text}</span>;
@@ -405,6 +424,7 @@ describe('ReactIncrementalSideEffects', () => {
424
]);
425
});
426
427
+ // @gate enableLegacyHiddenType
428
it('preserves a previously rendered node when deprioritized', () => {
429
function Middle(props) {
430
Scheduler.unstable_yieldValue('Middle');
@@ -415,9 +435,9 @@ describe('ReactIncrementalSideEffects', () => {
435
Scheduler.unstable_yieldValue('Foo');
436
return (
437
<div>
418
- <div hidden={true}>
438
+ <LegacyHiddenDiv hidden={true}>
439
<Middle>{props.text}</Middle>
420
- </div>
440
+ </LegacyHiddenDiv>
441
</div>
442
);
443
}
@@ -455,6 +475,7 @@ describe('ReactIncrementalSideEffects', () => {
475
);
476
});
477
478
+ // @gate enableLegacyHiddenType
479
it('can reuse side-effects after being preempted', () => {
480
function Bar(props) {
481
Scheduler.unstable_yieldValue('Bar');
@@ -471,7 +492,7 @@ describe('ReactIncrementalSideEffects', () => {
492
function Foo(props) {
493
Scheduler.unstable_yieldValue('Foo');
494
return (
474
- <div hidden={true}>
495
+ <LegacyHiddenDiv hidden={true}>
496
{props.step === 0 ? (
497
<div>
498
<Bar>Hi</Bar>
@@ -480,7 +501,7 @@ describe('ReactIncrementalSideEffects', () => {
501
) : (
502
middleContent
503
)}
483
- </div>
504
+ </LegacyHiddenDiv>
505
);
506
}
507
@@ -534,6 +555,7 @@ describe('ReactIncrementalSideEffects', () => {
555
);
556
});
557
558
+ // @gate enableLegacyHiddenType
559
it('can reuse side-effects after being preempted, if shouldComponentUpdate is false', () => {
560
class Bar extends React.Component {
561
shouldComponentUpdate(nextProps) {
@@ -563,9 +585,9 @@ describe('ReactIncrementalSideEffects', () => {
585
function Foo(props) {
586
Scheduler.unstable_yieldValue('Foo');
587
return (
566
- <div hidden={true}>
588
+ <LegacyHiddenDiv hidden={true}>
589
<Content step={props.step} text={props.text} />
568
- </div>
590
+ </LegacyHiddenDiv>
591
);
592
}
593
@@ -649,12 +671,13 @@ describe('ReactIncrementalSideEffects', () => {
671
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={3} />);
672
});
673
674
+ // @gate enableLegacyHiddenType
675
it('updates a child even though the old props is empty', () => {
676
function Foo(props) {
677
return (
655
- <div hidden={true}>
678
+ <LegacyHiddenDiv hidden={true}>
679
<span prop={1} />
657
- </div>
680
+ </LegacyHiddenDiv>
681
);
682
}
683
@@ -888,6 +911,7 @@ describe('ReactIncrementalSideEffects', () => {
911
expect(ops).toEqual(['Bar', 'Baz', 'Bar', 'Bar']);
912
});
913
914
+ // @gate enableLegacyHiddenType
915
it('deprioritizes setStates that happens within a deprioritized tree', () => {
916
const barInstances = [];
917
@@ -910,11 +934,11 @@ describe('ReactIncrementalSideEffects', () => {
934
return (
935
<div>
936
<span prop={props.tick} />
913
- <div hidden={true}>
937
+ <LegacyHiddenDiv hidden={true}>
938
<Bar idx={props.idx} />
939
<Bar idx={props.idx} />
940
<Bar idx={props.idx} />
917
- </div>
941
+ </LegacyHiddenDiv>
942
</div>
943
);
944
}
packages/react-reconciler/src/__tests__/ReactNewContext-test.js
+22
-2
@@ -42,6 +42,25 @@ describe('ReactNewContext', () => {
42
return dispatcher.readContext(Context, observedBits);
43
}
44
45
+ // TODO: Delete this once new API exists in both forks
46
+ function LegacyHiddenDiv({hidden, children, ...props}) {
47
+ if (gate(flags => flags.new)) {
48
+ return (
49
+ <div hidden={hidden} {...props}>
50
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
51
+ {children}
52
+ </React.unstable_LegacyHidden>
53
+ </div>
54
+ );
55
+ } else {
56
+ return (
57
+ <div hidden={hidden} {...props}>
58
+ {children}
59
+ </div>
60
+ );
61
+ }
62
+ }
63
+
64
// We have several ways of reading from context. sharedContextTests runs
65
// a suite of tests for a given context consumer implementation.
66
sharedContextTests('Context.Consumer', Context => Context.Consumer);
@@ -903,6 +922,7 @@ describe('ReactNewContext', () => {
922
expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
923
});
924
925
+ // @gate enableLegacyHiddenType
926
it("context consumer doesn't bail out inside hidden subtree", () => {
927
const Context = React.createContext('dark');
928
const Consumer = getConsumer(Context);
@@ -910,9 +930,9 @@ describe('ReactNewContext', () => {
930
function App({theme}) {
931
return (
932
<Context.Provider value={theme}>
913
- <div hidden={true}>
933
+ <LegacyHiddenDiv hidden={true}>
934
<Consumer>{value => <Text text={value} />}</Consumer>
915
- </div>
935
+ </LegacyHiddenDiv>
936
</Context.Provider>
937
);
938
}
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js
+22
-2
@@ -53,6 +53,25 @@ describe('ReactSchedulerIntegration', () => {
53
}
54
}
55
56
+ // TODO: Delete this once new API exists in both forks
57
+ function LegacyHiddenDiv({hidden, children, ...props}) {
58
+ if (gate(flags => flags.new)) {
59
+ return (
60
+ <div hidden={hidden} {...props}>
61
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
62
+ {children}
63
+ </React.unstable_LegacyHidden>
64
+ </div>
65
+ );
66
+ } else {
67
+ return (
68
+ <div hidden={hidden} {...props}>
69
+ {children}
70
+ </div>
71
+ );
72
+ }
73
+ }
74
+
75
it('flush sync has correct priority', () => {
76
function ReadPriority() {
77
Scheduler.unstable_yieldValue(
@@ -351,6 +370,7 @@ describe('ReactSchedulerIntegration', () => {
370
expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
371
});
372
373
+ // @gate enableLegacyHiddenType
374
it('idle updates are not blocked by offscreen work', async () => {
375
function Text({text}) {
376
Scheduler.unstable_yieldValue(text);
@@ -361,9 +381,9 @@ describe('ReactSchedulerIntegration', () => {
381
return (
382
<>
383
<Text text={`Visible: ` + label} />
364
- <div hidden={true}>
384
+ <LegacyHiddenDiv hidden={true}>
385
<Text text={`Hidden: ` + label} />
366
- </div>
386
+ </LegacyHiddenDiv>
387
</>
388
);
389
}
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+25
-4
@@ -142,6 +142,25 @@ describe('ReactSuspenseWithNoopRenderer', () => {
142
}
143
}
144
145
+ // TODO: Delete this once new API exists in both forks
146
+ function LegacyHiddenDiv({hidden, children, ...props}) {
147
+ if (gate(flags => flags.new)) {
148
+ return (
149
+ <div hidden={hidden} {...props}>
150
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
151
+ {children}
152
+ </React.unstable_LegacyHidden>
153
+ </div>
154
+ );
155
+ } else {
156
+ return (
157
+ <div hidden={hidden} {...props}>
158
+ {children}
159
+ </div>
160
+ );
161
+ }
162
+ }
163
+
164
it('does not restart rendering for initial render', async () => {
165
function Bar(props) {
166
Scheduler.unstable_yieldValue('Bar');
@@ -2891,6 +2910,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2910
});
2911
2912
// @gate experimental
2913
+ // @gate enableLegacyHiddenType
2914
it('should not render hidden content while suspended on higher pri', async () => {
2915
function Offscreen() {
2916
Scheduler.unstable_yieldValue('Offscreen');
@@ -2902,9 +2922,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2922
});
2923
return (
2924
<>
2905
- <div hidden={true}>
2925
+ <LegacyHiddenDiv hidden={true}>
2926
<Offscreen />
2907
- </div>
2927
+ </LegacyHiddenDiv>
2928
<Suspense fallback={<Text text="Loading..." />}>
2929
{showContent ? <AsyncText text="A" ms={2000} /> : null}
2930
</Suspense>
@@ -2946,6 +2966,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2966
});
2967
2968
// @gate experimental
2969
+ // @gate enableLegacyHiddenType
2970
it('should be able to unblock higher pri content before suspended hidden', async () => {
2971
function Offscreen() {
2972
Scheduler.unstable_yieldValue('Offscreen');
@@ -2957,10 +2978,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2978
});
2979
return (
2980
<Suspense fallback={<Text text="Loading..." />}>
2960
- <div hidden={true}>
2981
+ <LegacyHiddenDiv hidden={true}>
2982
<AsyncText text="A" ms={2000} />
2983
<Offscreen />
2963
- </div>
2984
+ </LegacyHiddenDiv>
2985
{showContent ? <AsyncText text="A" ms={2000} /> : null}
2986
</Suspense>
2987
);
packages/react-refresh/src/__tests__/ReactFresh-test.js
+21
-2
@@ -75,6 +75,25 @@ describe('ReactFresh', () => {
75
return type;
76
}
77
78
+ // TODO: Delete this once new API exists in both forks
79
+ function LegacyHiddenDiv({hidden, children, ...props}) {
80
+ if (gate(flags => flags.new)) {
81
+ return (
82
+ <div hidden={hidden} {...props}>
83
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
84
+ {children}
85
+ </React.unstable_LegacyHidden>
86
+ </div>
87
+ );
88
+ } else {
89
+ return (
90
+ <div hidden={hidden} {...props}>
91
+ {children}
92
+ </div>
93
+ );
94
+ }
95
+ }
96
+
97
it('can preserve state for compatible types', () => {
98
if (__DEV__) {
99
const HelloV1 = render(() => {
@@ -2417,9 +2436,9 @@ describe('ReactFresh', () => {
2436
Scheduler.unstable_yieldValue('App#layout');
2437
});
2438
return (
2420
- <div hidden={offscreen}>
2439
+ <LegacyHiddenDiv hidden={offscreen}>
2440
<Hello />
2422
- </div>
2441
+ </LegacyHiddenDiv>
2442
);
2443
};
2444
});
packages/react/index.classic.fb.js
+1
@@ -32,6 +32,7 @@ export {
32
Profiler,
33
StrictMode,
34
Suspense,
35
+ unstable_LegacyHidden,
36
createElement,
37
cloneElement,
38
isValidElement,
packages/react/index.experimental.js
+1
@@ -32,6 +32,7 @@ export {
32
Profiler,
33
StrictMode,
34
Suspense,
35
+ unstable_LegacyHidden,
36
createElement,
37
cloneElement,
38
isValidElement,
packages/react/index.js
+1
@@ -77,6 +77,7 @@ export {
77
unstable_withSuspenseConfig,
78
block,
79
block as unstable_block,
80
+ unstable_LegacyHidden,
81
DEPRECATED_useResponder,
82
DEPRECATED_createResponder,
83
unstable_createFundamental,
packages/react/index.modern.fb.js
+1
@@ -32,6 +32,7 @@ export {
32
Profiler,
33
StrictMode,
34
Suspense,
35
+ unstable_LegacyHidden,
36
createElement,
37
cloneElement,
38
isValidElement,
packages/react/src/React.js
+2
@@ -15,6 +15,7 @@ import {
15
REACT_STRICT_MODE_TYPE,
16
REACT_SUSPENSE_TYPE,
17
REACT_SUSPENSE_LIST_TYPE,
18
+ REACT_LEGACY_HIDDEN_TYPE,
19
} from 'shared/ReactSymbols';
20
21
import {Component, PureComponent} from './ReactBaseClasses';
@@ -110,6 +111,7 @@ export {
111
useTransition,
112
useDeferredValue,
113
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
114
+ REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
115
withSuspenseConfig as unstable_withSuspenseConfig,
116
// enableBlocksAPI
117
block,
packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+34
-10
@@ -55,6 +55,25 @@ function loadModules() {
55
});
56
}
57
58
+// TODO: Delete this once new API exists in both forks
59
+function LegacyHiddenDiv({hidden, children, ...props}) {
60
+ if (gate(flags => flags.new)) {
61
+ return (
62
+ <div hidden={hidden} {...props}>
63
+ <React.unstable_LegacyHidden mode={hidden ? 'hidden' : 'visible'}>
64
+ {children}
65
+ </React.unstable_LegacyHidden>
66
+ </div>
67
+ );
68
+ } else {
69
+ return (
70
+ <div hidden={hidden} {...props}>
71
+ {children}
72
+ </div>
73
+ );
74
+ }
75
+}
76
+
77
describe('ReactDOMTracing', () => {
78
beforeEach(() => {
79
jest.resetModules();
@@ -65,6 +84,7 @@ describe('ReactDOMTracing', () => {
84
describe('interaction tracing', () => {
85
describe('hidden', () => {
86
// @gate experimental
87
+ // @gate enableLegacyHiddenType
88
it('traces interaction through hidden subtree', () => {
89
const Child = () => {
90
const [didMount, setDidMount] = React.useState(false);
@@ -86,9 +106,9 @@ describe('ReactDOMTracing', () => {
106
Scheduler.unstable_yieldValue('App:mount');
107
}, []);
108
return (
89
- <div hidden={true}>
109
+ <LegacyHiddenDiv hidden={true}>
110
<Child />
91
- </div>
111
+ </LegacyHiddenDiv>
112
);
113
};
114
@@ -142,6 +162,7 @@ describe('ReactDOMTracing', () => {
162
});
163
164
// @gate experimental
165
+ // @gate enableLegacyHiddenType
166
it('traces interaction through hidden subtree when there is other pending traced work', () => {
167
const Child = () => {
168
Scheduler.unstable_yieldValue('Child');
@@ -157,9 +178,9 @@ describe('ReactDOMTracing', () => {
178
Scheduler.unstable_yieldValue('App:mount');
179
}, []);
180
return (
160
- <div hidden={true}>
181
+ <LegacyHiddenDiv hidden={true}>
182
<Child />
162
- </div>
183
+ </LegacyHiddenDiv>
184
);
185
};
186
@@ -210,6 +231,7 @@ describe('ReactDOMTracing', () => {
231
});
232
233
// @gate experimental
234
+ // @gate enableLegacyHiddenType
235
it('traces interaction through hidden subtree that schedules more idle/never work', () => {
236
const Child = () => {
237
const [didMount, setDidMount] = React.useState(false);
@@ -234,9 +256,9 @@ describe('ReactDOMTracing', () => {
256
Scheduler.unstable_yieldValue('App:mount');
257
}, []);
258
return (
237
- <div hidden={true}>
259
+ <LegacyHiddenDiv hidden={true}>
260
<Child />
239
- </div>
261
+ </LegacyHiddenDiv>
262
);
263
};
264
@@ -293,6 +315,7 @@ describe('ReactDOMTracing', () => {
315
});
316
317
// @gate experimental
318
+ // @gate enableLegacyHiddenType
319
it('does not continue interactions across pre-existing idle work', () => {
320
const Child = () => {
321
Scheduler.unstable_yieldValue('Child');
@@ -304,9 +327,9 @@ describe('ReactDOMTracing', () => {
327
const WithHiddenWork = () => {
328
Scheduler.unstable_yieldValue('WithHiddenWork');
329
return (
307
- <div hidden={true}>
330
+ <LegacyHiddenDiv hidden={true}>
331
<Child />
309
- </div>
332
+ </LegacyHiddenDiv>
333
);
334
};
335
@@ -394,6 +417,7 @@ describe('ReactDOMTracing', () => {
417
});
418
419
// @gate experimental
420
+ // @gate enableLegacyHiddenType
421
it('should properly trace interactions when there is work of interleaved priorities', () => {
422
const Child = () => {
423
Scheduler.unstable_yieldValue('Child');
@@ -411,9 +435,9 @@ describe('ReactDOMTracing', () => {
435
Scheduler.unstable_yieldValue('MaybeHiddenWork:effect');
436
});
437
return flag ? (
414
- <div hidden={true}>
438
+ <LegacyHiddenDiv hidden={true}>
439
<Child />
416
- </div>
440
+ </LegacyHiddenDiv>
441
) : null;
442
};
443
packages/shared/ReactFeatureFlags.js
+3
@@ -135,3 +135,6 @@ export const enableLegacyFBSupport = false;
135
// expiration time is currently rendering. Remove this flag once we have
136
// migrated to the new behavior.
137
export const deferRenderPhaseUpdateToNextBatch = true;
138
+
139
+// Flag used by www build so we can log occurrences of legacy hidden API
140
+export const warnAboutDOMHiddenAttribute = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -47,6 +47,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
47
48
export const enableNewReconciler = false;
49
export const deferRenderPhaseUpdateToNextBatch = true;
50
+export const warnAboutDOMHiddenAttribute = false;
51
52
// Flow magic to verify the exports of this file match the original version.
53
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46
47
export const enableNewReconciler = false;
48
export const deferRenderPhaseUpdateToNextBatch = true;
49
+export const warnAboutDOMHiddenAttribute = false;
50
51
// Flow magic to verify the exports of this file match the original version.
52
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46
47
export const enableNewReconciler = false;
48
export const deferRenderPhaseUpdateToNextBatch = true;
49
+export const warnAboutDOMHiddenAttribute = false;
50
51
// Flow magic to verify the exports of this file match the original version.
52
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46
47
export const enableNewReconciler = false;
48
export const deferRenderPhaseUpdateToNextBatch = true;
49
+export const warnAboutDOMHiddenAttribute = false;
50
51
// Flow magic to verify the exports of this file match the original version.
52
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46
47
export const enableNewReconciler = false;
48
export const deferRenderPhaseUpdateToNextBatch = true;
49
+export const warnAboutDOMHiddenAttribute = false;
50
51
// Flow magic to verify the exports of this file match the original version.
52
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46
47
export const enableNewReconciler = false;
48
export const deferRenderPhaseUpdateToNextBatch = true;
49
+export const warnAboutDOMHiddenAttribute = false;
50
51
// Flow magic to verify the exports of this file match the original version.
52
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+7
@@ -36,6 +36,13 @@ export const deferRenderPhaseUpdateToNextBatch = !__VARIANT__;
36
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
37
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
38
39
+// Do not add the corresponding warning to the warning filter! Only exists so we
40
+// can detect callers and migrate them to the new API. Should not visible to
41
+// anyone outside React Core team.
42
+//
43
+// Disabled in our tests, but we'll enable in www.
44
+export const warnAboutDOMHiddenAttribute = false;
45
+
46
// TODO: These flags are hard-coded to the default values used in open source.
47
// Update the tests so that they pass in either mode, then set these
48
// to __VARIANT__.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -27,6 +27,7 @@ export const {
27
enableLegacyFBSupport,
28
enableDebugTracing,
29
deferRenderPhaseUpdateToNextBatch,
30
+ warnAboutDOMHiddenAttribute,
31
} = dynamicFeatureFlags;
32
33
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
packages/shared/isValidElementType.js
+2
@@ -24,6 +24,7 @@ import {
24
REACT_SCOPE_TYPE,
25
REACT_BLOCK_TYPE,
26
REACT_SERVER_BLOCK_TYPE,
27
+ REACT_LEGACY_HIDDEN_TYPE,
28
} from 'shared/ReactSymbols';
29
30
export default function isValidElementType(type: mixed) {
@@ -37,6 +38,7 @@ export default function isValidElementType(type: mixed) {
38
type === REACT_STRICT_MODE_TYPE ||
39
type === REACT_SUSPENSE_TYPE ||
40
type === REACT_SUSPENSE_LIST_TYPE ||
41
+ type === REACT_LEGACY_HIDDEN_TYPE ||
42
(typeof type === 'object' &&
43
type !== null &&
44
(type.$$typeof === REACT_LAZY_TYPE ||
scripts/jest/TestFlags.js
+4
@@ -73,6 +73,10 @@ function getTestFlags() {
73
classic: releaseChannel === 'classic',
74
www,
75
76
+ // Using this more specific flag so it's easier to clean up later
77
+ enableLegacyHiddenType:
78
+ featureFlags.enableNewReconciler === false || __EXPERIMENTAL__,
79
+
80
...featureFlags,
81
...environmentFlags,
82
},