Animating appearance & disappearance in React Native

24 Apr 2017

Things don't just appear on the phone screen out of the blue. Showing the user how the UI elements fit together, where they come from, and where they go, is the cornerstone of a great user experience.

You may already know that animating the appearance of something can be straightforward. But what about when something needs to be hidden? If the component is already unmounted, how do you even animate its disappearance?

Seems... impossible, right?

But then, how does everyone have that? And not just everyone, but how can you make something like this?

How components are shown conditionally

Typically, to have a component either shown or hidden, depending on some state variable, we use expressions like this:

{
  showRectView && <RectView />;
}

But we don't want this component just to appear, we want to fade it in.

First shot at animating

Now that you want to somehow animate its appearance and disappearance, you could create a Fade component that would run the appearance animation, and use it like this:

{
  showRectView && (
    <Fade>
      <RectView />
    </Fade>
  );
}

The thing is, it would not animate on unmount — because once showRectView is false, Fade will be unmounted too, and will not have a chance to animate the view out.

What we want is the way to keep rendering RectView until the animation has finished. And the Fade component is the best place to handle that.

A different approach

Instead of conditionally rendering, we can pass a visible=true/false prop to Fade, and let it animate its children on both appearance and disappearance. The Fade component itself can be rendered at all times:

<Fade visible={showRectView}>
  <RectView />
</Fade>

Fade implementation

The simplest Fade could be implemented using these building blocks of React Native animations:

Want a handy little PDF to help you recall the building blocks? You can download the cheatsheet below. Otherwise keep reading.

Article continues:

import { Animated } from "react-native";

class Fade extends Component {
  componentWillMount() {
    this._visibility = new Animated.Value(this.props.visible ? 1 : 0);
  }

  componentWillReceiveProps(nextProps) {
    Animated.timing(this._visibility, {
      toValue: nextProps.visible ? 1 : 0,
      duration: 300,
    });
  }

  render() {
    const { visible, style, children, ...rest } = this.props;

    const containerStyle = {
      opacity: this._visibility.interpolate({
        inputRange: [0, 1],
        outputRange: [0, 1],
      }),
      transform: [
        {
          scale: this._visibility.interpolate({
            inputRange: [0, 1],
            outputRange: [1.1, 1],
          }),
        },
      ],
    };

    const combinedStyle = [containerStyle, style];
    return (
      <Animated.View style={combinedStyle} {...rest}>
        {children}
      </Animated.View>
    );
  }
}

Which is awesome already!

The thing is, it keeps rendering children with opacity = 0 even when visible=false. But we don't want that, of course.

One idea might be to render anything when this.props.visible is true, but this still has the drawbacks of the conditional rendering solution.

Instead, we need to be able to unmount the children after the animation has finished running.

To get there, we can keep a state property, called, quite uncreatively, visible. We will change it as follows:

constructor(props) {
  super(props);
  this.state = {
    visible: props.visible,
  };
};

componentWillReceiveProps(nextProps) {
  if (nextProps.visible) {
    this.setState({ visible: true });
  }
  Animated.timing(this._visibility, {
    toValue: nextProps.visible ? 1 : 0,
    duration: 300,
  }).start(() => {
    this.setState({ visible: nextProps.visible });
  });
  }

And now, you can get a nice appearance and disappearance animation by wrapping a component into:

// determine whether to show, possible based on state
const visible = ...

<Fade visible={visible}>
  <SomeComponent />
</Fade>

The full source of the Fade component would be:

import Animated from "react-native";

class Fade extends Component {
  constructor(props) {
    super(props);
    this.state = {
      visible: props.visible,
    };
  }

  componentWillMount() {
    this._visibility = new Animated.Value(this.props.visible ? 1 : 0);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.visible) {
      this.setState({ visible: true });
    }
    Animated.timing(this._visibility, {
      toValue: nextProps.visible ? 1 : 0,
      duration: 300,
    }).start(() => {
      this.setState({ visible: nextProps.visible });
    });
  }

  render() {
    const { visible, style, children, ...rest } = this.props;

    const containerStyle = {
      opacity: this._visibility.interpolate({
        inputRange: [0, 1],
        outputRange: [0, 1],
      }),
      transform: [
        {
          scale: this._visibility.interpolate({
            inputRange: [0, 1],
            outputRange: [1.1, 1],
          }),
        },
      ],
    };

    const combinedStyle = [containerStyle, style];
    return (
      <Animated.View style={this.state.visible ? combinedStyle : containerStyle} {...rest}>
        {this.state.visible ? children : null}
      </Animated.View>
    );
  }
}

Think your friends would dig this article, too?

Google+

Want to level up your React skills?

Sign up below and I'll send you articles just like this about React straight to your inbox every week or so.

No spam, promise. I hate it as much as you do!
If you need a mobile app built for your business or your idea, there's a chance I could help you with that.
Leave your email here and I will get back to you shortly.