Skip to content

Commit f34b304

Browse files
MatiPl01tomekzaw
andauthored
Add mutable value read/write during render warning (#6310)
## Summary This PR adds warnings shown when the SharedValue `.value` property is accessed (read or written) while the component is being rendered. The initial render is ignored as all reanimated hooks are executed for the first time during the initial render (e.g. `useAnimatedStyle` builds the initial component style). ## Test plan - open the **Invalid read/write during render** example in the example app, - press on the **Re-render** button to trigger re-render, which will access `.value` property of the shared value from the component function body, - see warnings about invalid read and invalid write --------- Co-authored-by: Tomek Zawadzki <[email protected]>
1 parent 8a2749e commit f34b304

File tree

4 files changed

+145
-1
lines changed

4 files changed

+145
-1
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Text, StyleSheet, View, Button } from 'react-native';
2+
3+
import React, { useEffect } from 'react';
4+
import Animated, {
5+
configureReanimatedLogger,
6+
useAnimatedStyle,
7+
useSharedValue,
8+
withTiming,
9+
} from 'react-native-reanimated';
10+
11+
configureReanimatedLogger({
12+
// change to `false` or remove the `configureReanimatedLogger` call to
13+
// disable the warning
14+
strict: true,
15+
});
16+
17+
export default function InvalidValueAccessExample() {
18+
const [counter, setCounter] = React.useState(0);
19+
const [updateFromUseEffect, setUpdateFromUseEffect] = React.useState(false);
20+
const sv = useSharedValue(0);
21+
22+
if (!updateFromUseEffect) {
23+
// logs writing to `value`... warning
24+
sv.value = counter;
25+
// logs reading from `value`... warning
26+
console.log('shared value:', sv.value);
27+
}
28+
29+
useEffect(() => {
30+
if (updateFromUseEffect) {
31+
// no warning is logged
32+
sv.value = counter;
33+
// no warning is logged
34+
console.log('useEffect shared value:', sv.value);
35+
}
36+
}, [sv, counter, updateFromUseEffect]);
37+
38+
const reRender = () => {
39+
setCounter((prev) => prev + 1);
40+
};
41+
42+
const animatedBarStyle = useAnimatedStyle(() => ({
43+
transform: [{ scaleX: withTiming(Math.sin((sv.value * Math.PI) / 10)) }],
44+
}));
45+
46+
return (
47+
<View style={styles.container}>
48+
<View style={styles.row}>
49+
<Text style={styles.text}>
50+
Update from:{' '}
51+
<Text style={styles.highlight}>
52+
{updateFromUseEffect ? 'useEffect' : 'component'}
53+
</Text>
54+
</Text>
55+
<Button
56+
title="change"
57+
onPress={() => setUpdateFromUseEffect((prev) => !prev)}
58+
/>
59+
</View>
60+
<Button title="Re-render" onPress={reRender} />
61+
<Text>Counter: {counter}</Text>
62+
<Animated.View style={[styles.bar, animatedBarStyle]} />
63+
</View>
64+
);
65+
}
66+
67+
const styles = StyleSheet.create({
68+
container: {
69+
flex: 1,
70+
alignItems: 'center',
71+
justifyContent: 'center',
72+
},
73+
bar: {
74+
height: 20,
75+
backgroundColor: 'blue',
76+
width: '100%',
77+
},
78+
text: {
79+
fontSize: 16,
80+
},
81+
highlight: {
82+
fontWeight: 'bold',
83+
},
84+
row: {
85+
flexDirection: 'row',
86+
alignItems: 'center',
87+
gap: 10,
88+
},
89+
});

apps/common-app/src/examples/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ import TabNavigatorExample from './SharedElementTransitions/TabNavigatorExample'
133133
import StrictDOMExample from './StrictDOMExample';
134134
import BottomTabsExample from './LayoutAnimations/BottomTabs';
135135
import ListItemLayoutAnimation from './LayoutAnimations/ListItemLayoutAnimation';
136+
import InvalidValueAccessExample from './InvalidValueAccessExample';
136137

137138
interface Example {
138139
icon?: string;
@@ -183,6 +184,11 @@ export const EXAMPLES: Record<string, Example> = {
183184
title: 'Freezing shareables',
184185
screen: FreezingShareablesExample,
185186
},
187+
InvalidReadWriteExample: {
188+
icon: '🔒',
189+
title: 'Invalid read/write during render',
190+
screen: InvalidValueAccessExample,
191+
},
186192

187193
// About
188194

packages/react-native-reanimated/src/mutables.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,37 @@
22
import { shouldBeUseWeb } from './PlatformChecker';
33
import type { Mutable } from './commonTypes';
44
import { ReanimatedError } from './errors';
5-
5+
import { logger } from './logger';
6+
import { isFirstReactRender, isReactRendering } from './reactUtils';
67
import { shareableMappingCache } from './shareableMappingCache';
78
import { makeShareableCloneRecursive } from './shareables';
89
import { executeOnUIRuntimeSync, runOnUI } from './threads';
910
import { valueSetter } from './valueSetter';
1011

1112
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
1213

14+
function shouldWarnAboutAccessDuringRender() {
15+
return __DEV__ && isReactRendering() && !isFirstReactRender();
16+
}
17+
18+
function checkInvalidReadDuringRender() {
19+
if (shouldWarnAboutAccessDuringRender()) {
20+
logger.warn(
21+
'Reading from `value` during component render. Please ensure that you do not access the `value` property while React is rendering a component.',
22+
{ strict: true }
23+
);
24+
}
25+
}
26+
27+
function checkInvalidWriteDuringRender() {
28+
if (shouldWarnAboutAccessDuringRender()) {
29+
logger.warn(
30+
'Writing to `value` during component render. Please ensure that you do not access the `value` property while React is rendering a component.',
31+
{ strict: true }
32+
);
33+
}
34+
}
35+
1336
type Listener<Value> = (newValue: Value) => void;
1437

1538
/**
@@ -93,12 +116,14 @@ function makeMutableNative<Value>(initial: Value): Mutable<Value> {
93116

94117
const mutable: Mutable<Value> = {
95118
get value(): Value {
119+
checkInvalidReadDuringRender();
96120
const uiValueGetter = executeOnUIRuntimeSync((sv: Mutable<Value>) => {
97121
return sv.value;
98122
});
99123
return uiValueGetter(mutable);
100124
},
101125
set value(newValue) {
126+
checkInvalidWriteDuringRender();
102127
runOnUI(() => {
103128
mutable.value = newValue;
104129
})();
@@ -146,9 +171,11 @@ function makeMutableWeb<Value>(initial: Value): Mutable<Value> {
146171

147172
const mutable: Mutable<Value> = {
148173
get value(): Value {
174+
checkInvalidReadDuringRender();
149175
return value;
150176
},
151177
set value(newValue) {
178+
checkInvalidWriteDuringRender();
152179
valueSetter(mutable, newValue);
153180
},
154181

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
import React from 'react';
3+
4+
function getCurrentReactOwner() {
5+
const ReactSharedInternals =
6+
// @ts-expect-error React secret internals aren't typed
7+
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ||
8+
// @ts-expect-error React secret internals aren't typed
9+
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
10+
return ReactSharedInternals?.ReactCurrentOwner?.current;
11+
}
12+
13+
export function isReactRendering() {
14+
return !!getCurrentReactOwner();
15+
}
16+
17+
export function isFirstReactRender() {
18+
const currentOwner = getCurrentReactOwner();
19+
// alternate is not null only after the first render and stores all the
20+
// data from the previous component render
21+
return currentOwner && !currentOwner?.alternate;
22+
}

0 commit comments

Comments
 (0)