Skip to content

[Web LA] Fix interaction with react-freeze - #7114

Merged
MatiPl01 merged 8 commits into
mainfrom
@mbert/fix-freeze
Jun 4, 2026
Merged

MatiPl01 merged 8 commits into
mainfrom
@mbert/fix-freeze

Conversation

@m-bert

@m-bert m-bert commented Feb 27, 2025 •

Copy link
Copy Markdown
Contributor

Summary

Preface

This issue was reported in Expensify.

In order for exiting animation to work, we have to prolong component's life. We do this by creating a copy of it and then assigning animation to the clone. The problem is that we cannot simply use element.cloneNode(true) as cloning children this way would create a copy, therefore if they also have exiting animation it would be assigned into original children. Here we have two issues:

  1. Original component is still visible
  2. Copy of children wouldn't have animation, while the original ones would animate on their initial position.

Problem 1. is solved by adding

  element.style.visibility = 'hidden';

Second one was solved by using the following loop:

  while (element.firstChild) {
    dummy.appendChild(element.firstChild);
  }

appendChild moves node into another place in the tree without making a copy. This approach made nested exiting animations easier.

react-freeze

There's a catch. With react-freeze, component is not unmounted from HTML tree - it just becomes invisible. When we unfreeze component, it goes back to being visible, but if it had exiting animation, its children were moved into its cloned. Therefore, if anything (for example React) would try to modify subtree of given node, it would throw error (for example using methods like insertBefore or removeChild)

Solution

To make exiting work with react-freeze, we add logic that once more performs reparenting, moving children again to its original parent after animation finishes (or gets cancelled).

If freeze was not used, then these children will be unmounted anyway, but if freeze was used, React can now properly interact with children.

Other possibilities

We also tried one more solution - clone children and place them in the original parent. However, it still throws errors. My guess is that either cloneNode doesn't create exact copy (using === operator yields false) or there are more conditions that have to be met when using methods like insertBefore.

Test plan

Tested on the following code:
import React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
import Animated, { FadeOutDown, FadeOutLeft } from 'react-native-reanimated';
import { Freeze } from 'react-freeze';

export default function EmptyExample() {
  const [freeze, setFreeze] = React.useState(false);

  const elementRef = React.useRef<HTMLElement>(null);
  const childRef = React.useRef<HTMLElement>(null);

  return (
    <View style={styles.container}>
      <View style={styles.buttonContainer}>
        <Button
          onPress={() => {
            if (!(elementRef && childRef)) {
              return;
            }

            const element = document.createElement('div');
            element.style.width = '70px';
            element.style.height = '70px';
            element.style.borderRadius = '35px';
            element.style.backgroundColor = 'crimson';

            elementRef.current?.insertBefore(element, childRef.current);
          }}
          title={'Add child'}
        />
        <Button
          onPress={() => setFreeze((prev) => !prev)}
          title={freeze ? 'Unfreeze' : 'Freeze'}
        />
      </View>

      <Freeze freeze={freeze} placeholder={<Text>Freeze</Text>}>
        <Animated.View
          ref={elementRef}
          style={styles.box}
          exiting={FadeOutLeft}>
          <Animated.View style={styles.outerBall} exiting={FadeOutDown} />
          <View ref={childRef} />
        </Animated.View>
      </Freeze>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },

  box: {
    width: 300,
    height: 300,
    backgroundColor: 'blue',
    borderRadius: 20,

    alignItems: 'center',
    justifyContent: 'center',
  },

  outerBall: {
    width: 100,
    height: 100,
    borderRadius: 50,
    backgroundColor: 'yellow',
    alignItems: 'center',
    justifyContent: 'center',
  },

  buttonContainer: {
    position: 'absolute',
    top: 100,
    left: 100,
  },
});

Copilot AI review requested due to automatic review settings April 15, 2026 11:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the web layout reanimation “exiting” flow to better interoperate with react-freeze by hiding the original element during exit and reparenting children back to the original element once the dummy’s exit animation completes/cancels.

Changes:

  • Hide the original element while its dummy clone performs the exiting animation.
  • Add cleanup logic to reparent children back to the original element after the dummy animation ends/cancels and remove the dummy.
  • Update setElementAnimation API to accept an originalElement reference for the exiting-animation cleanup path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@m-bert m-bert left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I've noticed that in the given repro, when adding child, at first yellow circle drops down to its previous position and then animates, but I think it is a case for other PR.

@m-bert
m-bert requested a review from MatiPl01 April 16, 2026 06:41

@MatiPl01 MatiPl01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be correct, thanks!

@MatiPl01
MatiPl01 added this pull request to the merge queue Jun 4, 2026
Merged via the queue into main with commit de1f2e2 Jun 4, 2026
12 checks passed
@MatiPl01
MatiPl01 deleted the @mbert/fix-freeze branch June 4, 2026 05:58
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.

3 participants