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

TypeError: Attempting to define property on object that is not extensible #2426

Closed
1 of 2 tasks
gentlee opened this issue Sep 14, 2021 · 9 comments
Closed
1 of 2 tasks

Comments

@gentlee
Copy link

gentlee commented Sep 14, 2021

Description

Got red error screen in iOS:

TypeError: Attempting to define property on object that is not extensible.

This error is located at:
    in AnimatedComponent(View) (at createAnimatedComponent.js:459)
    in ForwardRef(AnimatedComponentWrapper) (at StackItem.tsx:178)
    in PanGestureHandler (at StackItem.tsx:172)
    in ForwardRef
    in ForwardRef (at Stack.tsx:101)
    in RCTView (at View.js:34)
    in View (at Stack.tsx:115)
    in Unknown (at Swiper.tsx:271)
    in RCTView (at View.js:34)
    in View (at Swiper.tsx:285)
    in RCTView (at View.js:34)
    in View (at Swiper.tsx:283)
    in SwiperScreen (at SceneView.tsx:122)
    in StaticContainer
    in StaticContainer (at SceneView.tsx:115)
    in EnsureSingleNavigator (at SceneView.tsx:114)
    in SceneView (at useDescriptors.tsx:153)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at BottomNavigation.tsx:649)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at BottomNavigation.tsx:634)
    in RCTView (at View.js:34)
    in View (at BottomNavigation.tsx:609)
    in RCTView (at View.js:34)
    in View (at BottomNavigation.tsx:608)
    in BottomNavigation (created by Context.Consumer)
    in ThemedComponent (created by withTheme(BottomNavigation))
    in withTheme(BottomNavigation) (at MaterialBottomTabView.tsx:98)
    in MaterialBottomTabViewInner (at MaterialBottomTabView.tsx:198)
    in MaterialBottomTabView (at createMaterialBottomTabNavigator.tsx:45)
    in MaterialBottomTabNavigator (at main-bottom-tabs/index.tsx:39)
    in MainBottomTabs (at SceneView.tsx:122)
    in StaticContainer
    in StaticContainer (at SceneView.tsx:115)
    in EnsureSingleNavigator (at SceneView.tsx:114)
    in SceneView (at useDescriptors.tsx:153)
    in RCTView (at View.js:34)
    in View (at CardContainer.tsx:245)
    in RCTView (at View.js:34)
    in View (at CardContainer.tsx:244)
    in RCTView (at View.js:34)
    in View (at CardSheet.tsx:33)
    in ForwardRef(CardSheet) (at Card.tsx:573)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at Card.tsx:555)
    in PanGestureHandler (at GestureHandlerNative.tsx:13)
    in PanGestureHandler (at Card.tsx:549)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at Card.tsx:544)
    in RCTView (at View.js:34)
    in View (at Card.tsx:538)
    in Card (at CardContainer.tsx:206)
    in CardContainer (at CardStack.tsx:623)
    in RCTView (at View.js:34)
    in View (at Screens.tsx:84)
    in MaybeScreen (at CardStack.tsx:616)
    in RCTView (at View.js:34)
    in View (at Screens.tsx:54)
    in MaybeScreenContainer (at CardStack.tsx:498)
    in CardStack (at StackView.tsx:462)
    in KeyboardManager (at StackView.tsx:458)
    in SafeAreaProviderCompat (at StackView.tsx:455)
    in RCTView (at View.js:34)
    in View (at StackView.tsx:454)
    in StackView (at createStackNavigator.tsx:87)
    in StackNavigator (at main-stack-navigator/index.tsx:61)
    in MainStackNavigator (at SceneView.tsx:122)
    in StaticContainer
    in StaticContainer (at SceneView.tsx:115)
    in EnsureSingleNavigator (at SceneView.tsx:114)
    in SceneView (at useDescriptors.tsx:153)
    in RCTView (at View.js:34)
    in View (at CardContainer.tsx:245)
    in RCTView (at View.js:34)
    in View (at CardContainer.tsx:244)
    in RCTView (at View.js:34)
    in View (at CardSheet.tsx:33)
    in ForwardRef(CardSheet) (at Card.tsx:573)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at Card.tsx:555)
    in PanGestureHandler (at GestureHandlerNative.tsx:13)
    in PanGestureHandler (at Card.tsx:549)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at Card.tsx:544)
    in RCTView (at View.js:34)
    in View (at Card.tsx:538)
    in Card (at CardContainer.tsx:206)
    in CardContainer (at CardStack.tsx:623)
    in RCTView (at View.js:34)
    in View (at Screens.tsx:84)
    in MaybeScreen (at CardStack.tsx:616)
    in RCTView (at View.js:34)
    in View (at Screens.tsx:54)
    in MaybeScreenContainer (at CardStack.tsx:498)
    in CardStack (at StackView.tsx:462)
    in KeyboardManager (at StackView.tsx:458)
    in SafeAreaProviderCompat (at StackView.tsx:455)
    in RCTView (at View.js:34)
    in View (at StackView.tsx:454)
    in StackView (at createStackNavigator.tsx:87)
    in StackNavigator (at RootNavigator.tsx:64)
    in RootNavigator (at App.tsx:44)
    in EnsureSingleNavigator (at BaseNavigationContainer.tsx:409)
    in ForwardRef(BaseNavigationContainer) (at NavigationContainer.tsx:91)
    in ThemeProvider (at NavigationContainer.tsx:90)
    in ForwardRef(NavigationContainer) (at App.tsx:36)
    in RNCSafeAreaProvider (at SafeAreaContext.tsx:76)
    in SafeAreaProvider (at App.tsx:35)
    in RCTView (at View.js:34)
    in View (created by MenuProvider)
    in RCTView (at View.js:34)
    in View (created by MenuProvider)
    in MenuProvider (at App.tsx:34)
    in ApolloProvider (at App.tsx:29)
    in PersistGate (at redux-provider.tsx:9)
    in Provider (at redux-provider.tsx:8)
    in ReduxProvider (at App.tsx:28)
    in RCTView (at View.js:34)
    in View (at ActionSheet/index.ios.tsx:17)
    in ActionSheet (at ActionSheetProvider.tsx:31)
    in ActionSheetProvider (at App.tsx:27)
    in App (at renderApplication.js:45)
    in RCTView (at View.js:34)
    in View (at AppContainer.js:106)
    in RCTView (at View.js:34)
    in View (at AppContainer.js:132)
    in AppContainer (at renderApplication.js:39)

Expected behavior

No errors or better description.

Actual behavior & steps to reproduce

On app launch.

Snack or minimal code example

Probably problem is in one of these worklets:

const getAnimationEndX = (
  action: SwipeAction | undefined,
  windowWidth: number,
) => {
  'worklet';
  return action === SwipeAction.Like
    ? windowWidth * 2
    : action === SwipeAction.Dislike
    ? -windowWidth * 2
    : 0;
};

const getAnimationEndY = (
  action: SwipeAction | undefined,
  windowHeight: number,
) => {
  'worklet';
  return action === SwipeAction.SuperLike ? -windowHeight * 2 : 0;
};

// .. later inside component

const applyAction = (action: SwipeAction, triggerCallback = true) => {
  'worklet';

  translateX.value = withTiming(
    getAnimationEndX(action, windowWidth),
    DEFAULT_ANIMATION_CONFIG,
  );
  translateY.value = withTiming(
    getAnimationEndY(action, windowHeight),
    action === SwipeAction.SuperLike
      ? { duration: 700 }
      : DEFAULT_ANIMATION_CONFIG,
  );

  runOnJS(onSwiped)(item, action, triggerCallback);
};

All there worklets are called from both JS and UI threads.

Package versions

  • React Native: 0.63.4
  • React Native Reanimated: 2.2.0 and 2.2.1
  • NodeJS: v14.17.6
  • Xcode: 12.5.1
  • "react-native-gesture-handler": "1.10.3",

Affected platforms

  • [?] Android
  • iOS
  • Web
@github-actions
Copy link

Issue validator

The issue is valid!

@gentlee
Copy link
Author

gentlee commented Sep 14, 2021

So the problem is in gesture handler:

const gestureHandler = useAnimatedGestureHandler<
  PanGestureHandlerGestureEvent,
  { startX: number; startY: number }
>({
  onStart: (_, context) => {
    context.startX = translateX.value;
  },
  onActive: (event, context) => {
    translateX.value = context.startX + event.translationX;
  },
  onEnd: ({ translationX }) => {
    if (Math.abs(translationX) < labelOpacityInterpolation[1]) {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
      return;
    }

    applyAction(
      translationX > 0 ? SwipeAction.Like : SwipeAction.Dislike,
      true,
    );
  },
});

If I comment applyAction call it starts working.

@gentlee
Copy link
Author

gentlee commented Sep 15, 2021

This bug can be avoided by this hack:

// 1. create wrapper over onSwiped
const onSwipedImpl = (action: SwipeAction, triggerCallback: boolean) => {
  onSwiped(item, action, triggerCallback);
};

const applyAction = (action: SwipeAction, triggerCallback = true) => {
  'worklet';

  translateX.value = withTiming(
    getAnimationEndX(action, windowWidth),
    DEFAULT_ANIMATION_CONFIG,
  );
  translateY.value = withTiming(
    getAnimationEndY(action, windowHeight),
    action === SwipeAction.SuperLike
      ? { duration: 700 }
      : DEFAULT_ANIMATION_CONFIG,
  );

  // 2. call onSwipedImpl here
  runOnJS(onSwipedImpl)(action, triggerCallback);
};

@piaskowyk
Copy link
Member

Could you show your implementation of onSwiped?

@piaskowyk
Copy link
Member

@gentlee
So actually this is not a bug but it is just how worklet woks. I prepared a showcase to explain it. Look at this:

import React from 'react';
import { View, Button } from 'react-native';
import Animated, { useAnimatedGestureHandler } from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler';

function Component() {

  const testObject = {
    a: 1
  };

  const gestureHandler = useAnimatedGestureHandler({
    onStart: () => {
      'worklet'
      console.log("is UI:", _WORKLET)
      testObject.a = 2;
    }
  });

  const changeValue = () => {
    testObject.a = 2;
  };

  return (
    <View>
      <PanGestureHandler onGestureEvent={gestureHandler}>
        <Animated.View
          style={{ width: 200, height: 80, backgroundColor: 'black', margin: 30 }}
        />
      </PanGestureHandler>
      <Button title="click" onPress={() => changeValue()} />
    </View>
  );
}

export default Component;

onStart function inside useAnimatedGestureHandler is a worklet. onStart uses testObject - in other words onStart have testObject in own closure. Here starts the harder part. During creating a worklet we froze all variables/objects from worklet's closuer so after creating worklet testObject is immutable for other threads then UI thread - especially you can't change a value of an object from JS thread.
If you press button from this example you call to function changeValue and you try to change the value of the frozen object ant this operation throws TypeError: Attempted to assign to read-only property. error.

I think that exactly happens in your case. Especially if you pass item to worklet - React Component I guess - you froze the whole component.

Let me know if you have more questions.

@galgord
Copy link

galgord commented Sep 16, 2021

But how do we solve this?

@piaskowyk
Copy link
Member

piaskowyk commented Sep 16, 2021

@galgord It depends on your specific case. But generally, you need to avoid passing objects directly to the worklet if you want to modify them from other threads. Only SharedValue (useSharedValue) can be modified from any thread.
If you want to modify the object from the UI and JS thread instead of a passing object to the worklet you can pass the setter and call it in with runOnJS().

My example with working setter:

import React from 'react';
import { View, Button } from 'react-native';
import Animated, { useAnimatedGestureHandler, runOnJS } from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler';

function Component() {

  const testObject = {
    a: 1
  };

  const setter = (value) => {
    testObject.a = value;
  }

  const gestureHandler = useAnimatedGestureHandler({
    onStart: () => {
      'worklet'
      console.log("is UI:", _WORKLET)
      runOnJS(setter)(2);
    }
  });

  const changeValue = () => {
    testObject.a = 2;
  };

  return (
    <View>
      <PanGestureHandler onGestureEvent={gestureHandler}>
        <Animated.View
          style={{ width: 200, height: 80, backgroundColor: 'black', margin: 30 }}
        />
      </PanGestureHandler>
      <Button title="click" onPress={() => changeValue()} />
    </View>
  );
}

export default Component;

Is it the answer to your question?

@gentlee
Copy link
Author

gentlee commented Sep 27, 2021

@piaskowyk so to avoid this problem we need to create function that captures as much state that we need as possible, and capture this function from the worklet?

May be this note should be added to worklet documentation? Because i've spent a lot of time to fix that and docs didn't help me.

@gendalf-thug
Copy link

gendalf-thug commented Jun 12, 2022

@galgord It depends on your specific case. But generally, you need to avoid passing objects directly to the worklet if you want to modify them from other threads. Only SharedValue (useSharedValue) can be modified from any thread. If you want to modify the object from the UI and JS thread instead of a passing object to the worklet you can pass the setter and call it in with runOnJS().

My example with working setter:

import React from 'react';
import { View, Button } from 'react-native';
import Animated, { useAnimatedGestureHandler, runOnJS } from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler';

function Component() {

  const testObject = {
    a: 1
  };

  const setter = (value) => {
    testObject.a = value;
  }

  const gestureHandler = useAnimatedGestureHandler({
    onStart: () => {
      'worklet'
      console.log("is UI:", _WORKLET)
      runOnJS(setter)(2);
    }
  });

  const changeValue = () => {
    testObject.a = 2;
  };

  return (
    <View>
      <PanGestureHandler onGestureEvent={gestureHandler}>
        <Animated.View
          style={{ width: 200, height: 80, backgroundColor: 'black', margin: 30 }}
        />
      </PanGestureHandler>
      <Button title="click" onPress={() => changeValue()} />
    </View>
  );
}

export default Component;

Is it the answer to your question?

Worked for me thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants