software-mansion / react-native-reanimated

React Native's Animated library reimplemented
https://docs.swmansion.com/react-native-reanimated/
MIT License
8.95k stars 1.3k forks source link

Cannot assign to read-only property 'validated' #4942

Closed yolpsoftware closed 1 year ago

yolpsoftware commented 1 year ago

Description

After upgrading my Reanimated components to Reanimated 2 / 3, one of the components' animation behavior was not working in some cases. When I started the code in development mode (iOS simulator) to check out what was going wrong, I got a strange error message in the same Reanimated component:

 ERROR  TypeError: Cannot assign to read-only property 'validated'

This error is located at:
    in HeaderedListItem (created by App)
    in RNCSafeAreaProvider (created by SafeAreaProvider)
    in SafeAreaProvider (created by App)
    in RCTView (created by View)
    in View (created by GestureHandlerRootView)
    in GestureHandlerRootView (created by App)
    in ChildrenWrapper (created by Root)
    in _default (created by Root)
    in Root (created by RootSiblingParent)
    in RootSiblingParent (created by App)
    in App (created by withDevTools(App))
    in withDevTools(App)
    in RCTView (created by View)
    in View (created by AppContainer)
    in ChildrenWrapper (created by Root)
    in _default (created by Root)
    in Root (created by AppContainer)
    in RCTView (created by View)
    in View (created by AppContainer)
    in AppContainer
    in main(RootComponent), js engine: hermes

The error can be reproduced by the following minimal repro:

import React, { useCallback, useState } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { enableScreens } from 'react-native-screens';
import { RootSiblingParent } from 'react-native-root-siblings';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { View } from 'react-native';
import Animated, { SharedValue, interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';

if (!__DEV__) {
    enableScreens();
}

const App = () => {
    const scrollOffset = useSharedValue(0);
    return (
        <RootSiblingParent>
            <GestureHandlerRootView>
                <SafeAreaProvider>
                    <HeaderedListItem header={<View />} scrollValue={scrollOffset}>
                        <View style={{height: 200}} />
                    </HeaderedListItem>
                </SafeAreaProvider>
            </GestureHandlerRootView>
        </RootSiblingParent>
    );
}

export default App;

export const MIN_HEIGHT = 50;

export const HeaderedListItem = (props) => {
    const [yOffset, setYOffset] = useState(0);
    const [height, setHeight] = useState(MIN_HEIGHT);
    const style = useAnimatedStyle(() => ({
        transform: !props.scrollValue ? [] : [{
            translateY: interpolate(
                props.scrollValue.value,
                [-1000, yOffset, yOffset + Math.max(0, height - MIN_HEIGHT), yOffset + 10 + height + 100],
                [0, 0, height - MIN_HEIGHT, height - MIN_HEIGHT],
            )
        }],
    }), [props.scrollValue, yOffset, height]);
    const onLayout = useCallback((e) => {
        setYOffset(e.nativeEvent.layout.y - 100);
        setHeight(Math.max(e.nativeEvent.layout.height, MIN_HEIGHT));
    }, [setHeight, setYOffset]);
    return (
        <View onLayout={onLayout}>
            <Animated.View style={style}>
                <Animated.View />
                {props.header}
            </Animated.View>
            {props.children}
        </View>
    )
}

See also the following repository:

https://github.com/yolpsoftware/rea-3-bugrepro-836

The error seems to happen in the file react-jsx-runtime.development.js (line 1113), and it only happens in development mode, so it seems my animation problems in production might have a different cause.

Also, it seems that this file is a generic React file, and does not have a direct connection to Reanimated. However, as soon as you remove the useAnimatedStyle hook, the error does not happen anymore. That's why I posted it here. If you can reduce it to a minimal repro that does not contain any Reanimated code, I'm happy to submit it as a React or React Native bug.

Steps to reproduce

  1. Run the minimal repro code above, or checkout the repository https://github.com/yolpsoftware/rea-3-bugrepro-836 and run it.

Snack or a link to a repository

https://github.com/yolpsoftware/rea-3-bugrepro-836

Reanimated version

3.3.0

React Native version

0.72.3

Platforms

iOS

JavaScript runtime

Hermes

Workflow

Expo managed workflow

Architecture

Paper (Old Architecture)

Build type

Debug mode

Device

iOS simulator

Device model

No response

Acknowledgements

Yes

mvaivre commented 1 year ago

Witnessing the same issue in 3.3.0 as well.

More observations:

yolpsoftware commented 1 year ago

For me, it also happens on a full reload.

Maybe relevant: https://stackoverflow.com/questions/47553904/react-error-cannot-assign-to-read-only-property-validated-of-object-objec

Grohden commented 1 year ago

Have the same problem, I've noticed that the problem happens if I use useAnimatedStyle, and if I change how I reference a property the problem goes away, here's my example component:

```typescript import React, { useState } from 'react'; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { StyleSheet, View } from 'react-native'; const UNMEASURED = -1; export const RippleSurface = ( props: React.PropsWithChildren<{ rippleColor: string; }> ) => { const [radius, setRadius] = useState(UNMEASURED); const pressed = useSharedValue(false); const xAnimated = useSharedValue(1); const yAnimated = useSharedValue(1); const tap = Gesture.Tap() .onBegin((event) => { pressed.value = true; xAnimated.value = event.x; yAnimated.value = event.y; }) .onFinalize(() => { pressed.value = false; }); const animatedStyles = useAnimatedStyle(() => ({ borderRadius: radius, backgroundColor: props.rippleColor, width: radius * 2, height: radius * 2, opacity: withTiming(pressed.value ? 1 : 0), transform: [ { translateX: -radius }, { translateY: -radius }, { translateX: xAnimated.value }, { translateY: yAnimated.value }, { scale: withTiming(pressed.value ? 1 : 0.001) }, ], })); return ( { const { width, height } = event.nativeEvent.layout; setRadius(Math.sqrt(width ** 2 + height ** 2)); }} > {radius !== UNMEASURED && } {props.children} ); }; ```

if I apply these changes:

Index: example/src/components/ripple-surface/RippleSurface.tsx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/example/src/components/ripple-surface/RippleSurface.tsx b/example/src/components/ripple-surface/RippleSurface.tsx
--- a/example/src/components/ripple-surface/RippleSurface.tsx   (revision 3403801037346ebde8b1254d78b2b5fbe8a8eba2)
+++ b/example/src/components/ripple-surface/RippleSurface.tsx   (date 1692897605248)
@@ -28,9 +28,10 @@
       pressed.value = false;
     });

+  const rippleColor = props.rippleColor;
   const animatedStyles = useAnimatedStyle(() => ({
     borderRadius: radius,
-    backgroundColor: props.rippleColor,
+    backgroundColor: rippleColor,
     width: radius * 2,
     height: radius * 2,
     opacity: withTiming(pressed.value ? 1 : 0),

the error goes away... which leads me to believe that one of the RN reanimated babel plugins transforms any ref used inside of useAnimatedStyle and implicitly transforms it to a immutable dep, which in this case ends up transforming props

We can probably solve the problem by using more granular refs on those hooks.. although I think that the plugin should never do implicit things that change behavior of code..

yolpsoftware commented 1 year ago

@Grohden can confirm your workaround works well! Thanks a lot for sharing.

tjzel commented 1 year ago

Hi! This is a deliberate behavior of Reanimated. Take a look at this line:

https://github.com/software-mansion/react-native-reanimated/blob/main/src/reanimated2/shareables.ts#L224

We are freezing the whole props object in this case so users wouldn't expect updates of Animated Styles on change of plain JS values.

The way how plugin works, it adds to a worklet closure whole objects whose properties were accessed in worklet's body, not only their properties. We tried to have only those properties in the closure - but it raised many issues and we decided to use whole objects again.

In this scenario, props is having property children which you try to mount into React's tree. React does many things when mounting a component - it also changes its props. But since the object was frozen, it cannot do that and an error is thrown.

Unfortunately, when you face this issue it's very obscure. We plan to improve this mechanism and somehow point to Reanimated and explain what you should do here, but it's a quite complicated case.

What @Grohden provided here is not a workaround, but an actual proper solution. You should always try to use plain JS values as granularly as possible if you don't want them to be read-only - therefore unpacking your objects is a good choice here.

yolpsoftware commented 1 year ago

@tjzel thanks for the clarification.

Instead of freezing the props object, can't you replace its elements by properties (Object.defineProperty), and in the property setter, issue a warning, but only if the caller of the setter is another file than react-jsx-runtime.development.js? Not 100% sure though whether that is technically possible.

tjzel commented 1 year ago

I tried to but went into cases when for some reasons I couldn't use defineProperty on some props, there are some edge cases and I just decided to postpone it.

canpoyrazoglu commented 1 year ago

I'm having the same issue, any updates? What exactly is the solution now?

KrisLau commented 10 months ago

@tjzel I'm still getting this issue after updating from v3.1.0 to v3.6.0 even when I refactor my props as suggested:

import React from 'react';
import {Image, View} from 'react-native';
import {Icon, Text} from '@rneui/themed';
import PropTypes from 'prop-types';

import {Colors} from '../../../styles';
import styles from './styles';

function ListEmpty({custom, listItemName, customMessage, prompt, containerStyle}) {
  return (
    <View style={[styles.container, containerStyle]}>
      <Image
        source={require('../../../assets/images/nothing-found.png')}
        style={{backgroundColor: 'transparent'}}
      />
      <Text h4>{custom ? `${customMessage}` : `No ${listItemName} added yet!`}</Text>
      {prompt} //this
    </View>
  );
}

ListEmpty.defaultProps = {
  custom: false,
  listItemName: 'events',
  customMessage: '',
  prompt: (
    <Text style={styles.prompt}>
      Tap the <Icon size={15} name={'pluscircle'} type={'antdesign'} color={Colors.SECONDARY} /> to
      get started
    </Text>
  ),
  containerStyle: {},
};

ListEmpty.propTypes = {
  custom: PropTypes.bool,
  listItemName: PropTypes.string,
  customMessage: PropTypes.string,
  prompt: PropTypes.element,
  containerStyle: PropTypes.object || PropTypes.any,
};

export default ListEmpty;

commenting out the prompt from the component seems to get rid of the error but i have no clue why. It's annoying because i cant even downgrade the package because 3.1.0 doesn't work with RN0.72

cwooldridge1 commented 8 months ago

In my case the issue was that I had some animation based tailwind classes that I copied over from a react app into a react-native app and that I guess somewhere real deep internally did not like. Honestly sheer luck that I figured this out. Hope this can save someone else a couple hours.

mrsafalpiya commented 8 months ago

In my case the issue was that I had some animation based tailwind classes that I copied over from a react app into a react-native app and that I guess somewhere real deep internally did not like. Honestly sheer luck that I figured this out. Hope this can save someone else a couple hours.

Did you find any solution to this?

camilossantos2809 commented 7 months ago

In my case I have to change from const Component = ({prop1, prop2}: Props) => { to const Component = (props: Props) => { and use the props like props.prop1. Seems like that way the memory reference maintain the same.

Rc85 commented 6 months ago

Hi! This is a deliberate behavior of Reanimated. Take a look at this line:

https://github.com/software-mansion/react-native-reanimated/blob/main/src/reanimated2/shareables.ts#L224

We are freezing the whole props object in this case so users wouldn't expect updates of Animated Styles on change of plain JS values.

The way how plugin works, it adds to a worklet closure whole objects whose properties were accessed in worklet's body, not only their properties. We tried to have only those properties in the closure - but it raised many issues and we decided to use whole objects again.

In this scenario, props is having property children which you try to mount into React's tree. React does many things when mounting a component - it also changes its props. But since the object was frozen, it cannot do that and an error is thrown.

Unfortunately, when you face this issue it's very obscure. We plan to improve this mechanism and somehow point to Reanimated and explain what you should do here, but it's a quite complicated case.

What @Grohden provided here is not a workaround, but an actual proper solution. You should always try to use plain JS values as granularly as possible if you don't want them to be read-only - therefore unpacking your objects is a good choice here.

If it's a solution, will it be implemented?