Skip to content

feat: enable runtime shadow node reference updates for React commits#8776

Closed
jpudysz wants to merge 5 commits into
software-mansion:mainfrom
jpudysz:main
Closed

feat: enable runtime shadow node reference updates for React commits#8776
jpudysz wants to merge 5 commits into
software-mansion:mainfrom
jpudysz:main

Conversation

@jpudysz

@jpudysz jpudysz commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

Improve ShadowTree interoperability between Reanimated and 3rd party libraries

Hello Software Mansion team,

After countless hours of debugging and figuring out how to make the ShadowTree more shareable when using both Reanimated and Unistyles or Uniwind, I came up with a minimal solution that fixes all issues reported by the community.

I tested it across four repositories and the Uniwind Pro repository, which includes mappings for every React Native component (as shown in the videos below):

#7728
jpudysz/react-native-unistyles#1045
jpudysz/react-native-unistyles#888
jpudysz/react-native-unistyles#887

I decided to open a PR with a fix that I believe will satisfy everyone.

Introduction to the problem

We already discussed this in multiple GitHub threads, on the React Native Discord channel, and in person at conferences, but I think it is still worth explaining once more.

During the Unistyles 3.0 beta, I was using your approach with Commit and Mount hooks. As soon as I noticed performance issues, especially with ScrollView, I decided to look for another solution. I ended up with a single ShadowTree update, thanks to React Native 0.78 introducing runtime shadow node reference updates by default. Committing the ShadowTree update ensured that values were immediately accessible on every shadow node and after each ShadowTree clone.

After some time, I noticed new issues that I was fortunately able to fix using the nativeProps_DEPRECATED flag. This worked mostly fine, but relying on an internal Animated API with deprecated in its name is not a good long-term solution.

After the release of Reanimated 4.0, when ShadowTree updates were removed from the MountHook and uiManager exposed a new updateShadowTree method, we entered a new phase where we need to work together to figure out how both libraries can cooperate at the C++ level.

With all of this in mind, let’s look at Reanimated internals.

From my observations, Reanimated updates the ShadowTree through two paths. The first one is static (as I call it) where a linear animation starts and stops after some time. The second one is dynamic, where the animation is continuous and requires frequent ShadowTree updates.

Let’s consider two scenarios in which a user:

  1. Installs Unistyles and has a Pressable that triggers a theme change, resulting in a ShadowTree commit
  2. Experiences a native theme change

Static

In static mode, Reanimated uses the CommitHook exclusively, followed by the MountHook, with its own traits and props mapping.

image

If there is no node managed by Reanimated, everything works smoothly:

1.mov

As soon as at least one node is marked as Animated, commits are overridden:

2.mov

It works inconsistently, even on nodes that are not managed by Reanimated.

Dynamic

In this mode, Reanimated uses commitUpdates in ReanimatedModuleProxy, then the CommitHook, and finally the MountHook.

image 2

This case is rare, but still possible, for example when a user continuously animates a theme change icon, such as a spinning sun icon.

3.mov

What I tried

I tried a few solutions but wasn’t happy with the outcome.

Rely more on nativeProps_DEPRECATED

I tried updating your cloning mechanism to also copy unaffected siblings of affected nodes. It produced successful results, but I abandoned it because of the deprecated API. Additionally, it would slow down users who don’t use Unistyles or Uniwind.

std::shared_ptr<ShadowNode> cloneShadowTreeWithNewPropsRecursive(
    const ShadowNode &shadowNode,
    const ChildrenMap &childrenMap,
    const PropsMap &propsMap
) {
  const auto family = &shadowNode.getFamily();
  const auto affectedChildrenIt = childrenMap.find(family);
  auto children = shadowNode.getChildren();
  
    std::vector<bool> affectedChildren(children.size(), false);

    if (affectedChildrenIt != childrenMap.end()) {
        for (int index : affectedChildrenIt->second) {
            children[index] = cloneShadowTreeWithNewPropsRecursive(
                *children[index], childrenMap, propsMap
            );
            
            affectedChildren[index] = true;
        }
    }

    // forward nativeProps_DEPRECATED for unaffected children
    if (affectedChildrenIt != childrenMap.end()) {
        for (int i = 0; i < static_cast<int>(children.size()); i++) {
            if (affectedChildren[i]) {
                continue;
            }

            children[i] = cloneShadowTreeWithNewPropsRecursive(
                *children[i], childrenMap, propsMap
            );
        }
    }
    
  return shadowNode.clone(
      {mergeProps(shadowNode, propsMap, *family),
       std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(children),
       shadowNode.getState(),
       false});
}

Distinguish Unistyles/Uniwind commits

I use uiManager.updateShadowTree which defaults to ShadowTreeCommitSource::Unknown, and when I noticed the new feature flag (USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS) intended to improve performance, I hoped I could use this mechanism. Unfortunately, ScrollView also relies on it, so it is not exclusive to 3rd libraries and will be used by regular users.

I tried adding a custom trait, pausing for one frame by skipping a single Reanimated update, and similar approaches. The results worked only partially and were not satisfactory. I encountered visual flashes, among other issues and the solution required significant changes to your code, so I ultimately abandoned this idea.

Solution

Static

After another debugging session and comparing your algorithm with the React Native algorithm, I noticed that you don’t use Runtime Shadow Node Reference Updates RSNRU.

It was disabled a few months ago by @bartlomiejbloniarz:
Don't transfer reanimated updates to ReactJS (#7529)

I’m not sure when the animation is removed, but without this flag no other ShadowTree update can make it into the ShadowTree, because Reanimated immediately overrides it and treats it as its own commit.

With this flag enabled, everything works correctly, even though you still override the commit for managed nodes:

4.mov
5.mov

Additionally, I was able to drop nativeProps_DEPRECATED, so I no longer need to rely on a hidden React Native escape hatch.

Dynamic

This fix alone does not help with dynamic animations:

6.mov

After further investigation and exploring ReanimatedModuleProxy, I noticed that we can skip the ShadowTree commit path for non-layout properties, which makes total sense when switching themes.

Enabling both flags below gives excellent results:

"reanimated": {
    "staticFeatureFlags": {
        "IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS": true,
        "ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS": true
    }
}
7.mov
8.mov

Proposed solution

Initially, I wanted to introduce a new feature flag to address this, but if you need it for some removals, I believe we can enable it only when a Reanimated trait is present. For other sources, such as Unknown or React, it would simply be skipped.

I hope this makes sense and that it can land in the next release, as it would resolve all ongoing integration issues.

@bartlomiejbloniarz

Copy link
Copy Markdown
Contributor

Hi @jpudysz! Thanks for testing it so thoroughly. We wanted to introduce RSNRU to reanimated for some time as it would unlock huge performance benefits for our users. There are some problems with making the switch, that we want to first try to resolve. One of them is that it would break animation fill-mode in css animations. My hope is that we soon are able to migrate and your issue will be gone.

Using RSNRU in the commit hook is also a part of the Animation Backend in RN. So we will move there eventually.

I am worried though about the solution for the Dynamic path being just to use the synchronous updates feature flag. Synchronous updates are used only for non-layout styles, so wouldn't this break when you animate layout in the same way it breaks now?

@jpudysz

jpudysz commented Dec 18, 2025

Copy link
Copy Markdown
Contributor Author

Using RSNRU in the commit hook is also a react/react-native#54138 of the Animation Backend in RN. So we will move there eventually.

I saw it in the latest release notes, but it looks like you can update only a few properties right?

I am worried though about the solution for the Dynamic path being just to use the synchronous updates feature flag

Me too. I wasn’t sure why RSNRU doesn’t work there. I confirmed that my update runs in sequence between ticks, but somehow it gets lost.

Synchronous updates are used only for non-layout styles, so wouldn't this break when you animate layout in the same way it breaks now?

For most cases I’m trying to address, which are theme changes, it will work fine. Of course, if the user rotates the app while an animation is ongoing, we can see some Shadow Tree issues. To be honest, that would be fine for now, and we could wait for the AnimationBackend, which will allow you to remove the CommitHook.

I would be happy if you could find some time to check the dynamic path. I can put together a repro with the latest Reanimated and Unistyles.

@bartlomiejbloniarz

Copy link
Copy Markdown
Contributor

I saw it in the latest release notes, but it looks like you can update only a few properties right?

There are two ways props will be passed to the backend. One for some explicitly supported props, and others with folly::dynamic as a fallback. The coverage of the first type is being extended to cover the scope of BaseViewProps, more or less.

I would be happy if you could find some time to check the dynamic path. I can put together a repro with the latest Reanimated and Unistyles.

Yes, a repro would be great

@jpudysz

jpudysz commented Jan 5, 2026

Copy link
Copy Markdown
Contributor Author

Hey @bartlomiejbloniarz

I think I'm actually close to figuring out a solution without RSNRU that should prevent Reanimated from overriding things. Need to run a few more tests to make sure it's solid though.

Give me a few days to validate everything and I'll update you here with what I find.

Thanks again!

@jpudysz

jpudysz commented Jan 21, 2026

Copy link
Copy Markdown
Contributor Author

Ok, long story short, I was able to find a way to make it work quite reliably.
It works great in about 95% of cases, and in the remaining 5% it usually works as well, though I can still break it occasionally.

It can only fail when a ShadowTree update is used together with heavy, continuous Reanimated animations that push updates every frame. I think that is good enough, as almost no one would change the theme while running such heavy animations on the same screen.

I am eagerly waiting for AnimatedBackend, as it should fix all these issues (or create new ones 😅).

@jpudysz jpudysz closed this Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants