@samitouri / QOS-React-2 / commits / 8b9c4d1688

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 },