Compare commits

..

3 Commits

Author SHA1 Message Date
Mofei Zhang
0b8b8c6e1f [compiler] Inferred effect dependencies now include optional chains
Inferred effect dependencies now include optional chains.

This is a temporary solution while https://github.com/facebook/react/pull/32099 and its followups are worked on. Ideally, we should model reactive scope dependencies in the IR similarly to `ComputeIR` -- dependencies should be hoisted and all references rewritten to use the hoisted dependencies.

`
2025-05-22 16:00:45 -04:00
Mofei Zhang
90131ceca5 [compiler] Add reactive flag on scope dependencies
When collecting scope dependencies, mark each dependency with `reactive: true | false`. This prepares for later PRs https://github.com/facebook/react/pull/33326 and https://github.com/facebook/react/pull/32099 which rewrite scope dependencies into instructions.

Note that some reactive objects may have non-reactive properties, but we do not currently track this.

Technically, state[0] is reactive and state[1] is not. Currently, both would be marked as reactive.
```js
const state = useState();
```
2025-05-22 15:44:36 -04:00
Mofei Zhang
64b555c5fb [compiler] Prepare HIRBuilder to be used by later passes 2025-05-22 15:44:36 -04:00
12 changed files with 27 additions and 142 deletions

View File

@@ -41,10 +41,6 @@ const DynamicGatingOptionsSchema = z.object({
source: z.string(),
});
export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
const CustomOptOutDirectiveSchema = z
.nullable(z.array(z.string()))
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = {
environment: EnvironmentConfig;
@@ -136,11 +132,6 @@ export type PluginOptions = {
*/
ignoreUseNoForget: boolean;
/**
* Unstable / do not use
*/
customOptOutDirectives: CustomOptOutDirective;
sources: Array<string> | ((filename: string) => boolean) | null;
/**
@@ -287,7 +278,6 @@ export const defaultOptions: PluginOptions = {
return filename.indexOf('node_modules') === -1;
},
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
} as const;
@@ -348,21 +338,6 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
}
break;
}
case 'customOptOutDirectives': {
const result = CustomOptOutDirectiveSchema.safeParse(value);
if (result.success) {
parsedOptions[key] = result.data;
} else {
CompilerError.throwInvalidConfig({
reason:
'Could not parse custom opt out directives. Update React Compiler config to fix the error',
description: `${fromZodError(result.error)}`,
loc: null,
suggestions: null,
});
}
break;
}
default: {
parsedOptions[key] = value;
}

View File

@@ -63,16 +63,7 @@ export function tryFindDirectiveEnablingMemoization(
export function findDirectiveDisablingMemoization(
directives: Array<t.Directive>,
{customOptOutDirectives}: PluginOptions,
): t.Directive | null {
if (customOptOutDirectives != null) {
return (
directives.find(
directive =>
customOptOutDirectives.indexOf(directive.value.value) !== -1,
) ?? null
);
}
return (
directives.find(directive =>
OPT_OUT_DIRECTIVES.has(directive.value.value),
@@ -403,8 +394,7 @@ export function compileProgram(
code: pass.code,
suppressions,
hasModuleScopeOptOut:
findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
null,
findDirectiveDisablingMemoization(program.node.directives) != null,
});
const queue: Array<CompileSource> = findFunctionsToCompile(
@@ -581,10 +571,7 @@ function processFn(
}
directives = {
optIn: optIn.unwrapOr(null),
optOut: findDirectiveDisablingMemoization(
fn.node.body.directives,
programContext.opts,
),
optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
};
}

View File

@@ -93,21 +93,6 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
},
],
};
function* splitPragma(
pragma: string,
): Generator<{key: string; value: string | null}> {
for (const entry of pragma.split('@')) {
const keyVal = entry.trim();
const valIdx = keyVal.indexOf(':');
if (valIdx === -1) {
yield {key: keyVal.split(' ', 1)[0], value: null};
} else {
yield {key: keyVal.slice(0, valIdx), value: keyVal.slice(valIdx + 1)};
}
}
}
/**
* For snap test fixtures and playground only.
*/
@@ -116,11 +101,19 @@ function parseConfigPragmaEnvironmentForTest(
): EnvironmentConfig {
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
for (const {key, value: val} of splitPragma(pragma)) {
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
const valIdx = keyVal.indexOf(':');
const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
const isSet = val === undefined || val === 'true';
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
continue;
}
const isSet = val == null || val === 'true';
if (isSet && key in testComplexConfigDefaults) {
maybeConfig[key] = testComplexConfigDefaults[key];
} else if (isSet) {
@@ -183,11 +176,18 @@ export function parseConfigPragmaForTests(
compilationMode: defaults.compilationMode,
environment,
};
for (const {key, value: val} of splitPragma(pragma)) {
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
const idx = keyVal.indexOf(':');
const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
if (!hasOwnProperty(defaultOptions, key)) {
continue;
}
const isSet = val == null || val === 'true';
const isSet = val === undefined || val === 'true';
if (isSet && key in testComplexPluginOptionDefaults) {
options[key] = testComplexPluginOptionDefaults[key];
} else if (isSet) {

View File

@@ -1,35 +0,0 @@
## Input
```javascript
// @customOptOutDirectives:["use todo memo"]
function Component() {
'use todo memo';
return <div>hello world!</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Code
```javascript
// @customOptOutDirectives:["use todo memo"]
function Component() {
"use todo memo";
return <div>hello world!</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
### Eval output
(kind: ok) <div>hello world!</div>

View File

@@ -1,10 +0,0 @@
// @customOptOutDirectives:["use todo memo"]
function Component() {
'use todo memo';
return <div>hello world!</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};

View File

@@ -8344,23 +8344,6 @@ const testsTypescript = {
},
],
},
{
code: normalizeIndent`
function MyComponent(props) {
useEffect(() => {
console.log(props.foo);
});
}
`,
options: [{requireExplicitEffectDeps: true}],
errors: [
{
message:
'React Hook useEffect always requires dependencies. Please add a dependency array or an explicit `undefined`',
suggestions: undefined,
},
],
},
],
};

View File

@@ -67,9 +67,6 @@ const rule = {
type: 'string',
},
},
requireExplicitEffectDeps: {
type: 'boolean',
}
},
},
],
@@ -93,13 +90,10 @@ const rule = {
? rawOptions.experimental_autoDependenciesHooks
: [];
const requireExplicitEffectDeps: boolean = rawOptions && rawOptions.requireExplicitEffectDeps || false;
const options = {
additionalHooks,
experimental_autoDependenciesHooks,
enableDangerousAutofixThisMayCauseInfiniteLoops,
requireExplicitEffectDeps,
};
function reportProblem(problem: Rule.ReportDescriptor) {
@@ -1346,15 +1340,6 @@ const rule = {
return;
}
if (!maybeNode && isEffect && options.requireExplicitEffectDeps) {
reportProblem({
node: reactiveHook,
message:
`React Hook ${reactiveHookName} always requires dependencies. ` +
`Please add a dependency array or an explicit \`undefined\``
});
}
const isAutoDepsHook =
options.experimental_autoDependenciesHooks.includes(reactiveHookName);

View File

@@ -573,7 +573,7 @@ describe('ReactCompositeComponent-state', () => {
assertConsoleErrorDev([
"Can't perform a React state update on a component that hasn't mounted yet. " +
'This indicates that you have a side-effect in your render function that ' +
'asynchronously tries to update the component. ' +
'asynchronously later calls tries to update the component. ' +
'Move this work to useEffect instead.\n' +
' in B (at **)',
]);

View File

@@ -1922,7 +1922,7 @@ describe('ReactDOMServerPartialHydration', () => {
assertConsoleErrorDev([
"Can't perform a React state update on a component that hasn't mounted yet. " +
'This indicates that you have a side-effect in your render function that ' +
'asynchronously tries to update the component. Move this work to useEffect instead.\n' +
'asynchronously later calls tries to update the component. Move this work to useEffect instead.\n' +
' in App (at **)',
]);

View File

@@ -1883,7 +1883,7 @@ describe('ReactDOMServerPartialHydrationActivity', () => {
assertConsoleErrorDev([
"Can't perform a React state update on a component that hasn't mounted yet. " +
'This indicates that you have a side-effect in your render function that ' +
'asynchronously tries to update the component. Move this work to useEffect instead.\n' +
'asynchronously later calls tries to update the component. Move this work to useEffect instead.\n' +
' in App (at **)',
]);

View File

@@ -908,7 +908,7 @@ export function scheduleUpdateOnFiber(
markRootUpdated(root, lane);
if (
(executionContext & RenderContext) !== NoContext &&
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// This update was dispatched during the render phase. This is a mistake
@@ -4802,7 +4802,7 @@ export function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber: Fiber) {
console.error(
"Can't perform a React state update on a component that hasn't mounted yet. " +
'This indicates that you have a side-effect in your render function that ' +
'asynchronously tries to update the component. Move this work to ' +
'asynchronously later calls tries to update the component. Move this work to ' +
'useEffect instead.',
);
});

View File

@@ -764,7 +764,7 @@ describe('Activity', () => {
assertConsoleErrorDev([
"Can't perform a React state update on a component that hasn't mounted yet. " +
'This indicates that you have a side-effect in your render function that ' +
'asynchronously tries to update the component. ' +
'asynchronously later calls tries to update the component. ' +
'Move this work to useEffect instead.\n' +
' in Child (at **)',
]);