Skip to content

Commit

Permalink
Add mutable value read/write during render warning (#6310)
Browse files Browse the repository at this point in the history
## 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]>
  • Loading branch information
MatiPl01 and tomekzaw committed Aug 28, 2024
1 parent 8a2749e commit f34b304
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 1 deletion.
89 changes: 89 additions & 0 deletions apps/common-app/src/examples/InvalidValueAccessExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Text, StyleSheet, View, Button } from 'react-native';

import React, { useEffect } from 'react';
import Animated, {
configureReanimatedLogger,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';

configureReanimatedLogger({
// change to `false` or remove the `configureReanimatedLogger` call to
// disable the warning
strict: true,
});

export default function InvalidValueAccessExample() {
const [counter, setCounter] = React.useState(0);
const [updateFromUseEffect, setUpdateFromUseEffect] = React.useState(false);
const sv = useSharedValue(0);

if (!updateFromUseEffect) {
// logs writing to `value`... warning
sv.value = counter;
// logs reading from `value`... warning
console.log('shared value:', sv.value);
}

useEffect(() => {
if (updateFromUseEffect) {
// no warning is logged
sv.value = counter;
// no warning is logged
console.log('useEffect shared value:', sv.value);
}
}, [sv, counter, updateFromUseEffect]);

const reRender = () => {
setCounter((prev) => prev + 1);
};

const animatedBarStyle = useAnimatedStyle(() => ({
transform: [{ scaleX: withTiming(Math.sin((sv.value * Math.PI) / 10)) }],
}));

return (
<View style={styles.container}>
<View style={styles.row}>
<Text style={styles.text}>
Update from:{' '}
<Text style={styles.highlight}>
{updateFromUseEffect ? 'useEffect' : 'component'}
</Text>
</Text>
<Button
title="change"
onPress={() => setUpdateFromUseEffect((prev) => !prev)}
/>
</View>
<Button title="Re-render" onPress={reRender} />
<Text>Counter: {counter}</Text>
<Animated.View style={[styles.bar, animatedBarStyle]} />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
bar: {
height: 20,
backgroundColor: 'blue',
width: '100%',
},
text: {
fontSize: 16,
},
highlight: {
fontWeight: 'bold',
},
row: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
});
6 changes: 6 additions & 0 deletions apps/common-app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import TabNavigatorExample from './SharedElementTransitions/TabNavigatorExample'
import StrictDOMExample from './StrictDOMExample';
import BottomTabsExample from './LayoutAnimations/BottomTabs';
import ListItemLayoutAnimation from './LayoutAnimations/ListItemLayoutAnimation';
import InvalidValueAccessExample from './InvalidValueAccessExample';

interface Example {
icon?: string;
Expand Down Expand Up @@ -183,6 +184,11 @@ export const EXAMPLES: Record<string, Example> = {
title: 'Freezing shareables',
screen: FreezingShareablesExample,
},
InvalidReadWriteExample: {
icon: '🔒',
title: 'Invalid read/write during render',
screen: InvalidValueAccessExample,
},

// About

Expand Down
29 changes: 28 additions & 1 deletion packages/react-native-reanimated/src/mutables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,37 @@
import { shouldBeUseWeb } from './PlatformChecker';
import type { Mutable } from './commonTypes';
import { ReanimatedError } from './errors';

import { logger } from './logger';
import { isFirstReactRender, isReactRendering } from './reactUtils';
import { shareableMappingCache } from './shareableMappingCache';
import { makeShareableCloneRecursive } from './shareables';
import { executeOnUIRuntimeSync, runOnUI } from './threads';
import { valueSetter } from './valueSetter';

const SHOULD_BE_USE_WEB = shouldBeUseWeb();

function shouldWarnAboutAccessDuringRender() {
return __DEV__ && isReactRendering() && !isFirstReactRender();
}

function checkInvalidReadDuringRender() {
if (shouldWarnAboutAccessDuringRender()) {
logger.warn(
'Reading from `value` during component render. Please ensure that you do not access the `value` property while React is rendering a component.',
{ strict: true }
);
}
}

function checkInvalidWriteDuringRender() {
if (shouldWarnAboutAccessDuringRender()) {
logger.warn(
'Writing to `value` during component render. Please ensure that you do not access the `value` property while React is rendering a component.',
{ strict: true }
);
}
}

type Listener<Value> = (newValue: Value) => void;

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

const mutable: Mutable<Value> = {
get value(): Value {
checkInvalidReadDuringRender();
const uiValueGetter = executeOnUIRuntimeSync((sv: Mutable<Value>) => {
return sv.value;
});
return uiValueGetter(mutable);
},
set value(newValue) {
checkInvalidWriteDuringRender();
runOnUI(() => {
mutable.value = newValue;
})();
Expand Down Expand Up @@ -146,9 +171,11 @@ function makeMutableWeb<Value>(initial: Value): Mutable<Value> {

const mutable: Mutable<Value> = {
get value(): Value {
checkInvalidReadDuringRender();
return value;
},
set value(newValue) {
checkInvalidWriteDuringRender();
valueSetter(mutable, newValue);
},

Expand Down
22 changes: 22 additions & 0 deletions packages/react-native-reanimated/src/reactUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';
import React from 'react';

function getCurrentReactOwner() {
const ReactSharedInternals =
// @ts-expect-error React secret internals aren't typed
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ||
// @ts-expect-error React secret internals aren't typed
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
return ReactSharedInternals?.ReactCurrentOwner?.current;
}

export function isReactRendering() {
return !!getCurrentReactOwner();
}

export function isFirstReactRender() {
const currentOwner = getCurrentReactOwner();
// alternate is not null only after the first render and stores all the
// data from the previous component render
return currentOwner && !currentOwner?.alternate;
}

0 comments on commit f34b304

Please sign in to comment.