Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
yolpsoftware opened this issue Aug 20, 2023 · 13 comments
Closed

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

yolpsoftware opened this issue Aug 20, 2023 · 13 comments
Assignees
Labels
Needs review Issue is ready to be reviewed by a maintainer Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snippet of code, snack or repo is provided

Comments

@yolpsoftware
Copy link

yolpsoftware commented Aug 20, 2023

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

@yolpsoftware yolpsoftware added the Needs review Issue is ready to be reviewed by a maintainer label Aug 20, 2023
@github-actions github-actions bot added Repro provided A reproduction with a snippet of code, snack or repo is provided Platform: iOS This issue is specific to iOS labels Aug 20, 2023
@mvaivre
Copy link

mvaivre commented Aug 21, 2023

Witnessing the same issue in 3.3.0 as well.

More observations:

  • It may be linked to conditional animatedStyle
  • It only happens for me when refreshing (fast refresh). A full reload won't show the error.

@yolpsoftware
Copy link
Author

@Grohden
Copy link

Grohden commented Aug 24, 2023

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:

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 (
    <GestureDetector gesture={tap}>
      <Animated.View>
        <View
          style={{ ...StyleSheet.absoluteFillObject, overflow: 'hidden' }}
          onLayout={(event) => {
            const { width, height } = event.nativeEvent.layout;

            setRadius(Math.sqrt(width ** 2 + height ** 2));
          }}
        >
          {radius !== UNMEASURED && <Animated.View style={animatedStyles} />}
        </View>
        {props.children}
      </Animated.View>
    </GestureDetector>
  );
};

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
Copy link
Author

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

@tjzel
Copy link
Collaborator

tjzel commented Aug 28, 2023

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.

@tjzel tjzel closed this as completed Aug 28, 2023
@yolpsoftware
Copy link
Author

@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
Copy link
Collaborator

tjzel commented Aug 28, 2023

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
Copy link

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

@KrisLau
Copy link

KrisLau commented Nov 30, 2023

@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
Copy link

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
Copy link

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
Copy link
Contributor

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
Copy link

Rc85 commented Mar 29, 2024

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs review Issue is ready to be reviewed by a maintainer Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snippet of code, snack or repo is provided
Projects
None yet
Development

No branches or pull requests

10 participants