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

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

m-bert
Copy link
Contributor

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

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,
  },
});

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

Successfully merging this pull request may close these issues.

1 participant