Skip to content

Commit fb0bcde

Browse files
committed
[compiler] proof of concept to validate relay derived setStates
Summary: Experimenting with this a bit. Not a serious change yet
1 parent 7ab939b commit fb0bcde

File tree

9 files changed

+107
-20
lines changed

9 files changed

+107
-20
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,33 @@ import {TypeConfig} from './TypeSchema';
3939
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
4040
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
4141
*/
42+
43+
const RELAY_HOOK_TYPE: TypeConfig = {
44+
kind: 'hook',
45+
positionalParams: [],
46+
restParam: Effect.Freeze,
47+
returnType: {kind: 'type', name: 'BuiltInUseFragment'},
48+
returnValueKind: ValueKind.Frozen,
49+
noAlias: true,
50+
};
51+
4252
export function defaultModuleTypeProvider(
4353
moduleName: string,
4454
): TypeConfig | null {
4555
switch (moduleName) {
56+
case 'react-relay':
57+
case 'shared-runtime': {
58+
return {
59+
kind: 'object',
60+
properties: {
61+
useFragment: RELAY_HOOK_TYPE,
62+
usePaginationFragment: RELAY_HOOK_TYPE,
63+
useRefetchableFragment: RELAY_HOOK_TYPE,
64+
useLazyLoadQuery: RELAY_HOOK_TYPE,
65+
usePreloadedQuery: RELAY_HOOK_TYPE,
66+
},
67+
};
68+
}
4669
case 'react-hook-form': {
4770
return {
4871
kind: 'object',

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
} from './HIR';
4242
import {
4343
BuiltInMixedReadonlyId,
44+
BuiltInUseFragmentId,
4445
DefaultMutatingHook,
4546
DefaultNonmutatingHook,
4647
FunctionSignature,
@@ -819,14 +820,22 @@ export class Environment {
819820
],
820821
suggestions: null,
821822
});
823+
// Use BuiltInUseFragmentId for useFragment to enable tracking of fragment-derived values
824+
const returnTypeShapeId =
825+
hookName === 'useFragment'
826+
? BuiltInUseFragmentId
827+
: hook.transitiveMixedData
828+
? BuiltInMixedReadonlyId
829+
: null;
822830
this.#globals.set(
823831
hookName,
824832
addHook(this.#shapes, {
825833
positionalParams: [],
826834
restParam: hook.effectKind,
827-
returnType: hook.transitiveMixedData
828-
? {kind: 'Object', shapeId: BuiltInMixedReadonlyId}
829-
: {kind: 'Poly'},
835+
returnType:
836+
returnTypeShapeId != null
837+
? {kind: 'Object', shapeId: returnTypeShapeId}
838+
: {kind: 'Poly'},
830839
returnValueKind: hook.valueKind,
831840
calleeEffect: Effect.Read,
832841
hookKind: 'Custom',

compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
BuiltInUseContextHookId,
2121
BuiltInUseEffectEventId,
2222
BuiltInUseEffectHookId,
23+
BuiltInUseFragmentId,
2324
BuiltInUseInsertionEffectHookId,
2425
BuiltInUseLayoutEffectHookId,
2526
BuiltInUseOperatorId,
@@ -980,6 +981,9 @@ export function installTypeConfig(
980981
case 'MixedReadonly': {
981982
return {kind: 'Object', shapeId: BuiltInMixedReadonlyId};
982983
}
984+
case 'BuiltInUseFragment': {
985+
return {kind: 'Object', shapeId: BuiltInUseFragmentId};
986+
}
983987
case 'Primitive': {
984988
return {kind: 'Primitive'};
985989
}

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,10 @@ export function isUseStateType(id: Identifier): boolean {
18861886
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
18871887
}
18881888

1889+
export function isUseFragmentType(id: Identifier): boolean {
1890+
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseFragment';
1891+
}
1892+
18891893
export function isJsxType(type: Type): boolean {
18901894
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
18911895
}

compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ export const BuiltInSetActionStateId = 'BuiltInSetActionState';
392392
export const BuiltInUseRefId = 'BuiltInUseRefId';
393393
export const BuiltInRefValueId = 'BuiltInRefValue';
394394
export const BuiltInMixedReadonlyId = 'BuiltInMixedReadonly';
395+
export const BuiltInUseFragmentId = 'BuiltInUseFragment';
395396
export const BuiltInUseEffectHookId = 'BuiltInUseEffectHook';
396397
export const BuiltInUseLayoutEffectHookId = 'BuiltInUseLayoutEffectHook';
397398
export const BuiltInUseInsertionEffectHookId = 'BuiltInUseInsertionEffectHook';
@@ -1475,6 +1476,16 @@ addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
14751476
['*', {kind: 'Object', shapeId: BuiltInMixedReadonlyId}],
14761477
]);
14771478

1479+
/**
1480+
* BuiltInUseFragment represents values returned from Relay's useFragment hook.
1481+
* The catch-all property ensures that property accesses on useFragment data
1482+
* are also typed as BuiltInUseFragmentId, enabling tracking of fragment-derived
1483+
* values through the program.
1484+
*/
1485+
addObject(BUILTIN_SHAPES, BuiltInUseFragmentId, [
1486+
['*', {kind: 'Object', shapeId: BuiltInUseFragmentId}],
1487+
]);
1488+
14781489
addObject(BUILTIN_SHAPES, BuiltInJsxId, []);
14791490
addObject(BUILTIN_SHAPES, BuiltInFunctionId, []);
14801491

compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,15 @@ export type BuiltInTypeConfig =
294294
| 'Ref'
295295
| 'Array'
296296
| 'Primitive'
297-
| 'MixedReadonly';
297+
| 'MixedReadonly'
298+
| 'BuiltInUseFragment';
298299
export const BuiltInTypeSchema: z.ZodType<BuiltInTypeConfig> = z.union([
299300
z.literal('Any'),
300301
z.literal('Ref'),
301302
z.literal('Array'),
302303
z.literal('Primitive'),
303304
z.literal('MixedReadonly'),
305+
z.literal('BuiltInUseFragment'),
304306
]);
305307

306308
export type TypeReferenceConfig = {

compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
BuiltInPropsId,
3434
BuiltInRefValueId,
3535
BuiltInSetStateId,
36+
BuiltInUseFragmentId,
3637
BuiltInUseRefId,
3738
} from '../HIR/ObjectShape';
3839
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
@@ -859,12 +860,19 @@ function tryUnionTypes(ty1: Type, ty2: Type): Type | null {
859860
} else if (ty2.kind === 'Object' && ty2.shapeId === BuiltInMixedReadonlyId) {
860861
readonlyType = ty2;
861862
otherType = ty1;
863+
} else if (ty1.kind === 'Object' && ty1.shapeId === BuiltInUseFragmentId) {
864+
readonlyType = ty1;
865+
otherType = ty2;
866+
} else if (ty2.kind === 'Object' && ty2.shapeId === BuiltInUseFragmentId) {
867+
readonlyType = ty2;
868+
otherType = ty1;
862869
} else {
863870
return null;
864871
}
865872
if (otherType.kind === 'Primitive') {
866873
/**
867874
* Union(Primitive | MixedReadonly) = MixedReadonly
875+
* Union(Primitive | UseFragment) = UseFragment
868876
*
869877
* For example, `data ?? null` could return `data`, the fact that RHS
870878
* is a primitive doesn't guarantee the result is a primitive.
@@ -876,6 +884,7 @@ function tryUnionTypes(ty1: Type, ty2: Type): Type | null {
876884
) {
877885
/**
878886
* Union(Array | MixedReadonly) = Array
887+
* Union(Array | UseFragment) = Array
879888
*
880889
* In practice this pattern means the result is always an array. Given
881890
* that this behavior requires opting-in to the mixedreadonly type

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
IdentifierId,
1616
isSetStateType,
1717
isUseEffectHookType,
18+
isUseFragmentType,
1819
Place,
1920
CallExpression,
2021
Instruction,
@@ -28,7 +29,12 @@ import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
2829
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
2930
import {assertExhaustive} from '../Utils/utils';
3031

31-
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
32+
type TypeOfValue =
33+
| 'ignored'
34+
| 'fromProps'
35+
| 'fromState'
36+
| 'fromPropsAndState'
37+
| 'fromRelay';
3238

3339
type DerivationMetadata = {
3440
typeOfValue: TypeOfValue;
@@ -299,6 +305,9 @@ function joinValue(
299305
if (lvalueType === 'ignored') return valueType;
300306
if (valueType === 'ignored') return lvalueType;
301307
if (lvalueType === valueType) return lvalueType;
308+
if (lvalueType === 'fromRelay' || valueType === 'fromRelay') {
309+
return 'fromRelay';
310+
}
302311
return 'fromPropsAndState';
303312
}
304313

@@ -397,6 +406,16 @@ function recordInstructionDerivations(
397406
true,
398407
);
399408
return;
409+
} else if (isUseFragmentType(lvalue.identifier)) {
410+
console.log('relay');
411+
typeOfValue = 'fromRelay';
412+
context.derivationCache.addDerivationEntry(
413+
lvalue,
414+
new Set(),
415+
typeOfValue,
416+
true,
417+
);
418+
return;
400419
}
401420
} else if (value.kind === 'ArrayExpression') {
402421
context.candidateDependencies.set(lvalue.identifier.id, value);
@@ -796,6 +815,26 @@ function validateEffect(
796815
);
797816

798817
if (argMetadata !== undefined) {
818+
console.log(argMetadata.typeOfValue);
819+
// Check if the value being set is derived from useFragment (Relay)
820+
if (argMetadata.typeOfValue === 'fromRelay') {
821+
context.errors.pushDiagnostic(
822+
CompilerDiagnostic.create({
823+
description:
824+
'Deriving value from Relay store. Instead of holding it in a local state, use a client schema extension to store the value directly in the Relay store.',
825+
category: ErrorCategory.EffectDerivationsOfState,
826+
reason:
827+
'Avoid storing Relay data in React state. Use client schema extensions instead.',
828+
}).withDetails({
829+
kind: 'error',
830+
loc: instr.value.callee.loc,
831+
message:
832+
'setState with Relay-derived value. Use client schema instead.',
833+
}),
834+
);
835+
continue;
836+
}
837+
799838
effectDerivedSetStateCalls.push({
800839
value: instr.value,
801840
id: instr.value.callee.identifier.id,

packages/shared/ReactVersion.js

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1 @@
1-
/**
2-
* Copyright (c) Meta Platforms, Inc. and affiliates.
3-
*
4-
* This source code is licensed under the MIT license found in the
5-
* LICENSE file in the root directory of this source tree.
6-
*/
7-
8-
// TODO: this is special because it gets imported during build.
9-
//
10-
// It exists as a placeholder so that DevTools can support work tag changes between releases.
11-
// When we next publish a release, update the matching TODO in backend/renderer.js
12-
// TODO: This module is used both by the release scripts and to expose a version
13-
// at runtime. We should instead inject the version number as part of the build
14-
// process, and use the ReactVersions.js module as the single source of truth.
15-
export default '19.3.0';
1+
export default '19.3.0-canary--';

0 commit comments

Comments
 (0)