react-native-reanimated icon indicating copy to clipboard operation
react-native-reanimated copied to clipboard

Modal React Native break when using useAnimatedStyle in component

Open tamacroft opened this issue 1 year ago • 61 comments

Description

before Im upgrade to react native 0.76.1(new arch), Im using react native 0.75.4 and its work fine with old arch. I have trace error why my modal children is not rendered, because i have useAnimatedStyle in component where my modal placed. When i disabled useAnimatedStyle, its not break modal

Steps to reproduce

  1. I have card component, inside card is modal and bar component. Inside bar component im using useAnimatedStyle.
  2. Open modal inside card component
  3. Modal break (children not rendered)

Snack or a link to a repository

https://snack.expo.dev/@tamacroft_expo/2fd4f5

Reanimated version

3.16.1

React Native version

0.76.1

Platforms

Android

JavaScript runtime

Hermes

Workflow

React Native

Architecture

Fabric (New Architecture)

Build type

Debug app & production bundle

Device

Android emulator

Device model

No response

Acknowledgements

Yes

tamacroft avatar Nov 01 '24 02:11 tamacroft

This one is reported in react-native-screens and in Expo as well.

I've done some research and created a reproduction here.

A report was made in the RNScreens issue that this was working when using REA 3.14.0 and failed starting from REA 3.15.0.

After investigating the commits between the two releases I was able to find the offending PR here:

https://github.com/software-mansion/react-native-reanimated/pull/6214

This one is containing an optimization to how shadow nodes are updated and if we revert this optimization (even tried reverting it from 3.16.1 by doing some manual copy/paste) the issues is no longer happening.

bartlomiejbloniarz - would you be able to take a look?

This is related to the new architecture only.

chrfalch avatar Nov 01 '24 08:11 chrfalch

Putting here the visual recap of the impact on my app, from the Expo issue:

image

delphinebugner avatar Nov 04 '24 16:11 delphinebugner

@chrfalch Thanks for this investigation! I will take it from here.

bartlomiejbloniarz avatar Nov 07 '24 09:11 bartlomiejbloniarz

@chrfalch Thanks for this investigation! I will take it from here.

Awesome! Thanks :)

chrfalch avatar Nov 07 '24 09:11 chrfalch

@bartlomiejbloniarz hi, after upgrading to 0.76 I faced the same issue on Paper, for some reason the modal won't show up only on iOS though.

Can't give a reproducer since the code around showing the modal is kinda complex: an element is highlighted with:

  1. create a fade-in/fade-out svg rect
  2. show react-native-ui-lib dialog
  3. dialog does not show (uses modal under the hood).

Tried with different dialog components just in case react-native-ui-lib one is problematic, no luck, used rn Modal, no luck.

edit: When I display the dialog without the reanimated svg rect the dialog shows up just fine 🤷‍♂️

edit2: under some unknown circumstances, when hot reloading the Dialog shows up, feels like it's one commit behind.

efstathiosntonas avatar Nov 11 '24 10:11 efstathiosntonas

@efstathiosntonas Could you open a separate issue for that? In this one the Modal actually shows up, but some of its content is not visible. Also, the reason for this issue is heavily New Architecture dependent, so I don't think those problems could be related.

bartlomiejbloniarz avatar Nov 11 '24 14:11 bartlomiejbloniarz

@chrfalch I am still working on a solution. I have a (seemingly) working approach, but I'm still not 100% sure if it won't break anything.

The issue is that the non-visible content is actually rendered outside of the screen. This happens, because the Modal has wrong height. Usually the height is stored in the c++ state of RNSModalScreen and is updated after RNScreens obtain the height from iOS (this is why sometimes you can see a layout shift when a screen with a header is mounted).

Reanimated uses ReanimatedCommitHook to apply our animation changes on top of RN changes. To apply our changes, we clone ShadowNodes with new props. But whenever we clone a ShadowNode that has not been mounted, YogaLayoutableShadowNode re-clones all of its children. When cloning, RN uses the last mounted state, meaning that it actually overrides the height assigned by RNScreens, with wrong value.

Reverting the changes of #6214 doesn't actually solve the problem, as the old algorithm has the exact same flaw (but only in Release mode).

bartlomiejbloniarz avatar Nov 11 '24 14:11 bartlomiejbloniarz

Thanks!! Really appreciate you working on this @bartlomiejbloniarz - I didn't catch the release issue - so this was a good find.

chrfalch avatar Nov 12 '24 18:11 chrfalch

I am not sure that I understand the conversation here, I am having this issue with Android only. My app is on RN 0.76.2 and all dialogs/modals/bottom sheets are broken as they either do not show up or they get stuck all smushed in the top left corner and the whole screen becomes unusable. Is there a fix for this or something I can patch to make it work for now?

TweetyBoop1990 avatar Nov 21 '24 17:11 TweetyBoop1990

I am not sure that I understand the conversation here, I am having this issue with Android only. My app is on RN 0.76.2 and all dialogs/modals/bottom sheets are broken as they either do not show up or they get stuck all smushed in the top left corner and the whole screen becomes unusable. Is there a fix for this or something I can patch to make it work for now?

The only way I managed to fix this at is to just not have useAnimatedStyle used anywhere when a modal is shown.

Vali-98 avatar Nov 22 '24 08:11 Vali-98

Hi I have a related issue with Animated.View + Modal, in my case the screen is not cropped, but the modal has 0 width and 0 height, unless they are explicitly set.

Screenshot

Screenshot 2024-11-22 at 13 18 42

Repro code

import { useState } from "react";
import { Button, Modal, SafeAreaView, StatusBar, View } from "react-native";
import Animated, { useAnimatedStyle } from "react-native-reanimated";

export default function App() {
  const [visible, setVisible] = useState(false);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: 0 }],
  }));

  return (
    <SafeAreaView
      style={{
        flex: 1,
        backgroundColor: "#fff",
        padding: 16,
      }}
    >
      <StatusBar />
      <View
        style={{
          paddingVertical: 16,
        }}
      />
      <Button
        title="Open Modal"
        onPress={() => {
          setVisible(true);
        }}
      />
      <Animated.View style={animatedStyle} />
      <Modal visible={visible} animationType="slide">
        <View
          style={{
            padding: 32,
            backgroundColor: "pink",
          }}
        ></View>
      </Modal>
    </SafeAreaView>
  );
}

latekvo avatar Nov 22 '24 12:11 latekvo

@bartlomiejbloniarz Hey there, would you mind sharing your possible solution? I have been taking a stab at getting this to work with no good progress. Figured it'd be a good idea to ask from someone with partial success.

Vali-98 avatar Nov 25 '24 12:11 Vali-98

same problem

SetRedEyes avatar Nov 26 '24 12:11 SetRedEyes

Any updates on this issue?

sabuhiteymurov avatar Nov 27 '24 08:11 sabuhiteymurov

Hi @Vali-98, you can test the approach from this PR.

bartlomiejbloniarz avatar Dec 02 '24 12:12 bartlomiejbloniarz

@bartlomiejbloniarz Checked https://github.com/software-mansion/react-native-reanimated/pull/6776, issue with modals looks to be fixed.

mbilenko-florio avatar Dec 03 '24 19:12 mbilenko-florio

@bartlomiejbloniarz I can confirm that https://github.com/software-mansion/react-native-reanimated/pull/6776 fixed issue for me too.

patrik-majercik avatar Dec 05 '24 16:12 patrik-majercik

@bartlomiejbloniarz I can confirm that #6776 fixes this issue for me

eamccarron avatar Dec 16 '24 15:12 eamccarron

The problem still persists.

MelihOzyurt avatar Jan 08 '25 01:01 MelihOzyurt

same for me, when update my react native app from o.75.4 to 0.76.5, the modal not working in android, since on ios it work perfectly react-native-reanimated version is 3.16.1

TonyFTannous-Byd avatar Jan 08 '25 13:01 TonyFTannous-Byd

I encountered a similar issue where I have a modal that can be opened on every screen. However, on one specific screen - where I am also using an animated SVG Circle (with useAnimatedProps) - the modal does not become visible on Android, while it works fine on iOS.

The modal appears to be mounting correctly, as I cannot interact with elements on the screen when I set its visibility to true.

ilaloov avatar Jan 12 '25 08:01 ilaloov

I think I'm having a similar/related issue:

A normal <Modal ... (just plain content) works fine on the Android emulator.

When I add a <Swipeable ... from react-native-gesture-handler it seems to crunch up into the top left corner of the screen.

Removing the <Swipeable ... restores the modal (just deleting the component, the import statement is still there)

davidcarboni avatar Jan 13 '25 19:01 davidcarboni

I am facing a similar issue after our repo upgraded the RN version to 0.76. The react navigation modals are not as before, for iPhone I see the cropped bottom as mentioned before, and for iPad a weird behavior, like if the content wrapper get bigger than the modal when you have some interaction with it.

(Edit) We were able to "fix" the issue, previously we were using the navigation header that react navigation provides, after change for our custom header the issue is not happening anymore.

https://github.com/user-attachments/assets/4b52859a-aeb6-462a-b0d0-b106a5cc1f90

https://github.com/user-attachments/assets/28fb1ad2-fbc8-4dd3-bb94-c5e7dd8cacc1

joaolfr avatar Jan 22 '25 14:01 joaolfr

I am facing a similar issue when I am using useAnimatedStyle on the same page with a Modal. the Modal appears to be displayed above the screen view, with only the very bottom showing. When the Modal is made visible, this also freezes the App.

As an aside, this very page works with Reanimated 3.6.1 and React Native 0.72.

s1tony avatar Jan 24 '25 23:01 s1tony

any update on this issue ? i am on latest version of react native and when i use useAnimatedStyle specially transforms modal content does not render i only see a overlay of modal

    const animatedStyle = useAnimatedStyle(() => ({
        transform: [
            {
                translateX: boxX.value,
            },
            {
                translateY: boxY.value,
            },
            {
                rotate: `${boxRotation.value}rad`,
            },
        ],
        height: boxHeight.value,
        width: boxWidth.value,
    }));

//if i remove the transform part everything works well 

Kaizodo avatar Jan 28 '25 07:01 Kaizodo

I was having the issue with Modal + useAnimatedStyle.

I'm using:

  • Expo SDK 52 + New Architecture
  • Reanimated ~3.16.7

PR #6776 fix it for me.

@s1tony @Kaizodo you guys could test it and give feedback.

Use patch-package to apply:

patches/react-native-reanimated+3.16.7.patch

diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp
index 4ad8463..70ccf81 100644
--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp
+++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp
@@ -78,7 +78,7 @@ RootShadowNode::Unshared ReanimatedCommitHook::shadowTreeWillCommit(
           propsMap[&family].emplace_back(props);
         });
 
-    rootNode = cloneShadowTreeWithNewProps(*rootNode, propsMap);
+    rootNode = cloneShadowTreeWithNewPropsUnmounted(rootNode, propsMap);
 
     // If the commit comes from React Native then pause commits from
     // Reanimated since the ShadowTree to be committed by Reanimated may not
diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp
index 5795b73..02637a6 100644
--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp
+++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp
@@ -1,5 +1,6 @@
 #ifdef RCT_NEW_ARCH_ENABLED
 
+#include <react/renderer/core/DynamicPropsUtilities.h>
 #include <reanimated/Fabric/ShadowTreeCloner.h>
 
 #include <ranges>
@@ -7,6 +8,29 @@
 
 namespace reanimated {
 
+ChildrenMap calculateChildrenMap(
+    const RootShadowNode &oldRootNode,
+    const PropsMap &propsMap) {
+  ChildrenMap childrenMap;
+
+  for (auto &[family, _] : propsMap) {
+    const auto ancestors = family->getAncestors(oldRootNode);
+
+    for (const auto &[parentNode, index] :
+         std::ranges::reverse_view(ancestors)) {
+      const auto parentFamily = &parentNode.get().getFamily();
+      auto &affectedChildren = childrenMap[parentFamily];
+
+      if (affectedChildren.contains(index)) {
+        continue;
+      }
+
+      affectedChildren.insert(index);
+    }
+  }
+  return childrenMap;
+}
+
 ShadowNode::Unshared cloneShadowTreeWithNewPropsRecursive(
     const ShadowNode &shadowNode,
     const ChildrenMap &childrenMap,
@@ -43,33 +67,92 @@ ShadowNode::Unshared cloneShadowTreeWithNewPropsRecursive(
   return result;
 }
 
-RootShadowNode::Unshared cloneShadowTreeWithNewProps(
-    const RootShadowNode &oldRootNode,
+ShadowNode::Unshared cloneShadowTreeWithNewPropsUnmountedRecursive(
+    ShadowNode::Shared const &oldShadowNode,
+    const ChildrenMap &childrenMap,
     const PropsMap &propsMap) {
-  ChildrenMap childrenMap;
+  if (oldShadowNode->getHasBeenPromoted()) {
+    return cloneShadowTreeWithNewPropsRecursive(
+        *oldShadowNode, childrenMap, propsMap);
+  }
 
-  for (auto &[family, _] : propsMap) {
-    const auto ancestors = family->getAncestors(oldRootNode);
+  auto shadowNode = std::const_pointer_cast<ShadowNode>(oldShadowNode);
+  auto layoutableShadowNode =
+      std::dynamic_pointer_cast<LayoutableShadowNode>(shadowNode);
+  if (layoutableShadowNode) {
+    layoutableShadowNode->dirtyLayout();
+  }
 
-    for (const auto &[parentNode, index] :
-         std::ranges::reverse_view(ancestors)) {
-      const auto parentFamily = &parentNode.get().getFamily();
-      auto &affectedChildren = childrenMap[parentFamily];
+  const auto family = &shadowNode->getFamily();
+  const auto affectedChildrenIt = childrenMap.find(family);
+  const auto propsIt = propsMap.find(family);
+  auto children = shadowNode->getChildren();
 
-      if (affectedChildren.contains(index)) {
-        continue;
+  if (affectedChildrenIt != childrenMap.end()) {
+    for (const auto index : affectedChildrenIt->second) {
+      auto clone = cloneShadowTreeWithNewPropsUnmountedRecursive(
+          children[index], childrenMap, propsMap);
+      if (clone != children[index]) {
+        shadowNode->replaceChild(*children[index], clone, index);
       }
+    }
+  }
 
-      affectedChildren.insert(index);
+  Props::Shared newProps = nullptr;
+
+  if (propsIt != propsMap.end()) {
+    PropsParserContext propsParserContext{
+        shadowNode->getSurfaceId(), *shadowNode->getContextContainer()};
+    newProps = shadowNode->getProps();
+    for (const auto &props : propsIt->second) {
+      newProps = shadowNode->getComponentDescriptor().cloneProps(
+          propsParserContext, newProps, RawProps(props));
     }
   }
 
+  if (newProps) {
+    auto &props = shadowNode->getProps();
+    auto &mutableProps = const_cast<Props::Shared &>(props);
+
+#ifdef ANDROID
+    auto &newPropsRef = const_cast<Props &>(*newProps);
+    newPropsRef.rawProps = mergeDynamicProps(
+        mutableProps->rawProps,
+        newProps->rawProps,
+        NullValueStrategy::Override);
+#endif
+    mutableProps = newProps;
+    auto layoutableShadowNode =
+        static_pointer_cast<YogaLayoutableShadowNode>(shadowNode);
+    layoutableShadowNode->updateYogaProps();
+  }
+
+  return shadowNode;
+}
+
+RootShadowNode::Unshared cloneShadowTreeWithNewProps(
+    const RootShadowNode &oldRootNode,
+    const PropsMap &propsMap) {
+  auto childrenMap = calculateChildrenMap(oldRootNode, propsMap);
+
   // This cast is safe, because this function returns a clone
   // of the oldRootNode, which is an instance of RootShadowNode
   return std::static_pointer_cast<RootShadowNode>(
       cloneShadowTreeWithNewPropsRecursive(oldRootNode, childrenMap, propsMap));
 }
 
+RootShadowNode::Unshared cloneShadowTreeWithNewPropsUnmounted(
+    RootShadowNode::Unshared const &oldRootNode,
+    const PropsMap &propsMap) {
+  auto childrenMap = calculateChildrenMap(*oldRootNode, propsMap);
+
+  // This cast is safe, because this function returns a clone
+  // of the oldRootNode, which is an instance of RootShadowNode
+  return std::static_pointer_cast<RootShadowNode>(
+      cloneShadowTreeWithNewPropsUnmountedRecursive(
+          oldRootNode, childrenMap, propsMap));
+}
+
 } // namespace reanimated
 
 #endif // RCT_NEW_ARCH_ENABLED
diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.h b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.h
index e3f9f6d..8240776 100644
--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.h
+++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.h
@@ -24,6 +24,10 @@ RootShadowNode::Unshared cloneShadowTreeWithNewProps(
     const RootShadowNode &oldRootNode,
     const PropsMap &propsMap);
 
+RootShadowNode::Unshared cloneShadowTreeWithNewPropsUnmounted(
+    RootShadowNode::Unshared const &oldRootShadowNode,
+    const PropsMap &propsMap);
+
 } // namespace reanimated
 
 #endif // RCT_NEW_ARCH_ENABLED

wfern avatar Feb 04 '25 18:02 wfern

Initial testing looks promising - have a few more things to test, will keep you updated.

Thank you @wfern

I was having the issue with Modal + useAnimatedStyle.

I'm using:

* Expo SDK 52 + New Architecture

* Reanimated ~3.16.7

PR #6776 fix it for me.

@s1tony @Kaizodo you guys could test it and give feedback.

Use patch-package to apply:

patches/react-native-reanimated+3.16.7.patch

s1tony avatar Feb 04 '25 21:02 s1tony

@wfern After the patch it only works but not properly , sometimes it shows content mostly first attempt but afterwards its stops working randomly.

Kaizodo avatar Feb 05 '25 12:02 Kaizodo

After further testing, I am experiencing a similar problem, but for me, the content usually does not display on the first attempt, but after several attempts.

s1tony avatar Feb 05 '25 12:02 s1tony

Same issue

maksymhcode-care avatar Feb 06 '25 12:02 maksymhcode-care