@samitouri / QOS-React-1 / commits / af28f480e8

Feature flag to disable legacy context for function components (#30319)

While the goal is to remove legacy context completely, I think we can already land the removal of legacy context for function components. I didn't even know this feature existed until reading the code recently. The win is just a couple of property lookups on function renders, but it trims down the API already as the full removal will likely still take a bit more time. www: Starting with enabled test renderer and a feature flag for production rollout. RN: Not enabled, will follow up on this.

Jan Kassens committed Jul 11, 2024 at 16:21 UTC af28f480e8e74ce71bb33259b61fef8a5a228f74
15 files changed +48 -9
packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js
+24
@@ -86,6 +86,9 @@ describe('ReactDOMServerIntegration', () => {
86 });
87
88 itRenders('stateless child with context', async render => {
89 + if (gate(flags => flags.disableLegacyContextForFunctionComponents)) {
90 + return;
91 + }
92 function FunctionChildWithContext(props, context) {
93 return <div>{context.text}</div>;
94 }
@@ -118,6 +121,9 @@ describe('ReactDOMServerIntegration', () => {
121 });
122
123 itRenders('stateless child without context', async render => {
124 + if (gate(flags => flags.disableLegacyContextForFunctionComponents)) {
125 + return;
126 + }
127 function FunctionChildWithoutContext(props, context) {
128 // this should render blank; context isn't passed to this component.
129 return <div>{context.text}</div>;
@@ -151,6 +157,9 @@ describe('ReactDOMServerIntegration', () => {
157 });
158
159 itRenders('stateless child with wrong context', async render => {
160 + if (gate(flags => flags.disableLegacyContextForFunctionComponents)) {
161 + return;
162 + }
163 function FunctionChildWithWrongContext(props, context) {
164 // this should render blank; context.text isn't passed to this component.
165 return <div id="statelessWrongChild">{context.text}</div>;
@@ -169,6 +178,9 @@ describe('ReactDOMServerIntegration', () => {
178 });
179
180 itRenders('with context passed through to a grandchild', async render => {
181 + if (gate(flags => flags.disableLegacyContextForFunctionComponents)) {
182 + return;
183 + }
184 function Grandchild(props, context) {
185 return <div>{context.text}</div>;
186 }
@@ -186,6 +198,9 @@ describe('ReactDOMServerIntegration', () => {
198 });
199
200 itRenders('a child context overriding a parent context', async render => {
201 + if (gate(flags => flags.disableLegacyContextForFunctionComponents)) {
202 + return;
203 + }
204 const Grandchild = (props, context) => {
205 return <div>{context.text}</div>;
206 };
@@ -203,6 +218,9 @@ describe('ReactDOMServerIntegration', () => {
218 });
219
220 itRenders('a child context merged with a parent context', async render => {
221 + if (gate(flags => flags.disableLegacyContextForFunctionComponents)) {
222 + return;
223 + }
224 class Parent extends React.Component {
225 getChildContext() {
226 return {text1: 'purple'};
@@ -244,6 +262,9 @@ describe('ReactDOMServerIntegration', () => {
262 itRenders(
263 'with a call to componentWillMount before getChildContext',
264 async render => {
265 + if (gate(flags => flags.disableLegacyContextForFunctionComponents)) {
266 + return;
267 + }
268 class WillMountContext extends React.Component {
269 getChildContext() {
270 return {text: this.state.text};
@@ -270,6 +291,9 @@ describe('ReactDOMServerIntegration', () => {
291 itRenders(
292 'if getChildContext exists but childContextTypes is missing with a warning',
293 async render => {
294 + if (gate(flags => flags.disableLegacyContextForFunctionComponents)) {
295 + return;
296 + }
297 function HopefulChild(props, context) {
298 return context.foo || 'nope';
299 }
packages/react-dom/src/__tests__/ReactFunctionComponent-test.js
+1 -1
@@ -451,7 +451,7 @@ describe('ReactFunctionComponent', () => {
451 ]);
452 });
453
454 - // @gate !disableLegacyContext
454 + // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
455 it('should receive context', async () => {
456 class Parent extends React.Component {
457 static childContextTypes = {
packages/react-reconciler/src/ReactFiberBeginWork.js
+2 -1
@@ -95,6 +95,7 @@ import {
95 import {
96 debugRenderPhaseSideEffectsForStrictMode,
97 disableLegacyContext,
98 + disableLegacyContextForFunctionComponents,
99 enableProfilerCommitHooks,
100 enableProfilerTimer,
101 enableScopeAPI,
@@ -1158,7 +1159,7 @@ function updateFunctionComponent(
1159 }
1160
1161 let context;
1161 - if (!disableLegacyContext) {
1162 + if (!disableLegacyContext && !disableLegacyContextForFunctionComponents) {
1163 const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
1164 context = getMaskedContext(workInProgress, unmaskedContext);
1165 }
packages/react-reconciler/src/__tests__/ReactIncremental-test.js
+3 -3
@@ -1701,7 +1701,7 @@ describe('ReactIncremental', () => {
1701 expect(instance.state.n).toEqual(3);
1702 });
1703
1704 - // @gate !disableLegacyContext
1704 + // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
1705 it('merges and masks context', async () => {
1706 class Intl extends React.Component {
1707 static childContextTypes = {
@@ -1954,7 +1954,7 @@ describe('ReactIncremental', () => {
1954 ]);
1955 });
1956
1957 - // @gate !disableLegacyContext
1957 + // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
1958 it('reads context when setState is below the provider', async () => {
1959 let statefulInst;
1960
@@ -2046,7 +2046,7 @@ describe('ReactIncremental', () => {
2046 assertLog([]);
2047 });
2048
2049 - // @gate !disableLegacyContext
2049 + // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
2050 it('reads context when setState is above the provider', async () => {
2051 let statefulInst;
2052
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+1 -1
@@ -1158,7 +1158,7 @@ describe('ReactIncrementalErrorHandling', () => {
1158 // because it's used for new context, suspense, and many other features.
1159 // It has to be tested independently for each feature anyway. So although it
1160 // doesn't look like it, this test is specific to legacy context.
1161 - // @gate !disableLegacyContext
1161 + // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
1162 it('unwinds the context stack correctly on error', async () => {
1163 class Provider extends React.Component {
1164 static childContextTypes = {message: PropTypes.string};
packages/react-reconciler/src/__tests__/ReactUse-test.js
+1 -1
@@ -1562,7 +1562,7 @@ describe('ReactUse', () => {
1562 expect(root).toMatchRenderedOutput('Async!');
1563 });
1564
1565 - // @gate !disableLegacyContext
1565 + // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
1566 it('unwrap uncached promises in component that accesses legacy context', async () => {
1567 class ContextProvider extends React.Component {
1568 static childContextTypes = {
packages/react-server/src/ReactFizzServer.js
+2 -1
@@ -152,6 +152,7 @@ import {
152 import ReactSharedInternals from 'shared/ReactSharedInternals';
153 import {
154 disableLegacyContext,
155 + disableLegacyContextForFunctionComponents,
156 enableScopeAPI,
157 enableSuspenseAvoidThisFallbackFizz,
158 enableCache,
@@ -1654,7 +1655,7 @@ function renderFunctionComponent(
1655 props: any,
1656 ): void {
1657 let legacyContext;
1657 - if (!disableLegacyContext) {
1658 + if (!disableLegacyContext && !disableLegacyContextForFunctionComponents) {
1659 legacyContext = getMaskedContext(Component, task.legacyContext);
1660 }
1661 if (__DEV__) {
packages/shared/ReactFeatureFlags.js
+7 -1
@@ -155,8 +155,14 @@ export const transitionLaneExpirationMs = 5000;
155 // Renames the internal symbol for elements since they have changed signature/constructor
156 export const renameElementSymbol = true;
157
158 -// Removes legacy style context
158 +/**
159 + * Removes legacy style context defined using static `contextTypes` and consumed with static `childContextTypes`.
160 + */
161 export const disableLegacyContext = true;
162 +/**
163 + * Removes legacy style context just from function components.
164 + */
165 +export const disableLegacyContextForFunctionComponents = true;
166
167 // Not ready to break experimental yet.
168 // Modern <StrictMode /> behaviour aligns more with what components
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -37,6 +37,7 @@ export const disableCommentsAsDOMContainers = true;
37 export const disableIEWorkarounds = true;
38 export const disableInputAttributeSyncing = false;
39 export const disableLegacyContext = false;
40 +export const disableLegacyContextForFunctionComponents = false;
41 export const disableLegacyMode = false;
42 export const disableSchedulerTimeoutInWorkLoop = false;
43 export const disableStringRefs = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -28,6 +28,7 @@ export const disableDefaultPropsExceptForClasses = true;
28 export const disableIEWorkarounds = true;
29 export const disableInputAttributeSyncing = false;
30 export const disableLegacyContext = true;
31 +export const disableLegacyContextForFunctionComponents = true;
32 export const disableLegacyMode = false;
33 export const disableSchedulerTimeoutInWorkLoop = false;
34 export const disableStringRefs = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -92,6 +92,7 @@ export const disableStringRefs = true;
92 export const enableFastJSX = true;
93 export const disableLegacyMode = true;
94 export const disableLegacyContext = true;
95 +export const disableLegacyContextForFunctionComponents = true;
96 export const enableRenderableContext = true;
97 export const enableReactTestRendererWarning = true;
98 export const disableDefaultPropsExceptForClasses = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1
@@ -20,6 +20,7 @@ export const disableDefaultPropsExceptForClasses = false;
20 export const disableIEWorkarounds = true;
21 export const disableInputAttributeSyncing = false;
22 export const disableLegacyContext = false;
23 +export const disableLegacyContextForFunctionComponents = false;
24 export const disableLegacyMode = false;
25 export const disableSchedulerTimeoutInWorkLoop = false;
26 export const disableStringRefs = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -32,6 +32,7 @@ export const enableScopeAPI = true;
32 export const enableCreateEventHandleAPI = false;
33 export const enableSuspenseCallback = true;
34 export const disableLegacyContext = false;
35 +export const disableLegacyContextForFunctionComponents = false;
36 export const enableTrustedTypesIntegration = false;
37 export const disableTextareaChildren = false;
38 export const enableSuspenseAvoidThisFallback = true;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -15,6 +15,7 @@
15
16 export const alwaysThrottleRetries = true;
17 export const disableDefaultPropsExceptForClasses = __VARIANT__;
18 +export const disableLegacyContextForFunctionComponents = __VARIANT__;
19 export const disableLegacyMode = __VARIANT__;
20 export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
21 export const enableAddPropertiesFastPath = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -17,6 +17,7 @@ const dynamicFeatureFlags: DynamicFeatureFlags = require('ReactFeatureFlags');
17 export const {
18 alwaysThrottleRetries,
19 disableDefaultPropsExceptForClasses,
20 + disableLegacyContextForFunctionComponents,
21 disableSchedulerTimeoutInWorkLoop,
22 enableAddPropertiesFastPath,
23 enableDebugTracing,