fix(linter): treat Reanimated shared values as ref-like in React Compiler rules

Closes #26222
This commit is contained in:
Boshen committed 2026-10-29 15:21:32 +08:00
1 parent 22923f274e
commit 4c9d5aa496
5 files changed
+89

No files matched your search

@@ -0,0 +1,5 @@
{
"categories": { "correctness": "off" },
"plugins": ["react"],
"rules": { "react/immutability": "error", "react/refs": "error" }
}
@@ -0,0 +1,36 @@
import { useEffect } from "react";
import { Gesture } from "react-native-gesture-handler";
import { useSharedValue, withSpring, withTiming } from "react-native-reanimated";
// Shared values imported from Reanimated are ref-like, so writing them outside
// of render is allowed.
export function SomeComponent() {
const sharedVal = useSharedValue(0);
return <Button onPress={() => (sharedVal.value = Math.random())} title="Randomize" />;
}
export function usePressToGrow() {
const width = useSharedValue(100);
return () => {
width.value = withSpring(width.value + 50);
};
}
export function useFadeIn(visible: boolean) {
const opacity = useSharedValue(0);
useEffect(() => {
opacity.value = withTiming(visible ? 1 : 0);
}, [opacity, visible]);
return opacity;
}
export function useDrag() {
const offset = useSharedValue(0);
return Gesture.Pan()
.onChange((event) => {
offset.value += event.changeX;
})
.onFinalize(() => {
offset.value = withSpring(0);
});
}
@@ -0,0 +1,6 @@
// A global `useSharedValue` is an unknown hook, so writing its result is still
// reported.
export function SomeComponent() {
const sharedVal = useSharedValue(0);
return <Button onPress={() => (sharedVal.value = Math.random())} title="Randomize" />;
}
@@ -0,0 +1,38 @@
# Exit code
1
# stdout
```
x react(immutability): Cannot modify local variables after render completes
,-[files/non-imported.jsx:5:34]
4 | const sharedVal = useSharedValue(0);
5 | return <Button onPress={() => (sharedVal.value = Math.random())} title="Randomize" />;
: ^| ^^^^|^^^^
: | `-- This modifies `sharedVal`
: `-- This function may (indirectly) reassign or modify `sharedVal` after render
6 | }
`----
help: This argument is a function which may reassign or mutate `sharedVal` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/immutability
x react(immutability): This value cannot be modified
,-[files/non-imported.jsx:5:34]
3 | export function SomeComponent() {
4 | const sharedVal = useSharedValue(0);
: ^^^^|^^^^
: `-- `sharedVal` originates here
5 | return <Button onPress={() => (sharedVal.value = Math.random())} title="Randomize" />;
: ^^^^|^^^^
: `-- `sharedVal` cannot be modified
6 | }
`----
help: Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/immutability
Found 0 warnings and 2 errors.
Finished in Xms on 2 files with 2 rules using X threads.
```
# stderr
```
```
@@ -69,6 +69,10 @@ fn react_compiler_plugin_options(env_options: &ReactCompilerEnvOptions) -> Plugi
validate_no_derived_computations_in_effects: true,
validate_exhaustive_memoization_dependencies: true,
validate_exhaustive_effect_dependencies: ExhaustiveEffectDepsMode::All,
// The Babel plugin enables this when it detects `react-native-reanimated`, which
// Oxlint cannot do without a plugin list or resolver. The type model only applies
// to values imported from `react-native-reanimated`, so enabling it is safe.
enable_custom_type_definition_for_reanimated: true,
..EnvironmentConfig::default()
},
lint_skip_reactive_scope_passes: !env_options.report_todo_and_invariant,