React Video Tutorial: Animating React Components


In this tutorial, I invite you to discover together how to animate your React components.

The structure of React is not very conducive to the creation of animations because with the virtual-dom we want our structure to reflect our state. If we introduce a disappearance effect, an element can remain visible a little while after its disappearance in the state.

Several possible approaches

Use a third-party library

React has many libraries to create animations. They are very interesting but are generally very heavy and quite unsuitable for simple cases.

  • Framer motion is the most complete library (but also the heaviest).
  • React spring, uses hooks and animation functions based on springs.
  • React transition group, allows to have a base to create animations in CSS.

Also, for simple cases it is sometimes more relevant to create a simple homemade component.

Alternate a CSS class

The first idea is to use a simple class to handle the animation.

.fade {
  transition: opacity 1s;
}

.fade.out {
  opacity: 0;
}

And we apply or remove it as needed.

function Fade ({visible, children}) {
  const className = `fade $ {visible? "": "out"} `.trim ();
  return 
{children}
; }

The downside is that the element remains present in the DOM (which can be a problem in a display grid) and in the virtual DOM (which will generate unnecessary renderings).
We can then add a state to our component so as not to make the component a child.

function Fade ({visible, children, duration = 300}) {
  const (showChildren, setShowChildren) = useState (visible);

  useEffect (() => {
    if (visible) {
      setShowChildren (true);
    } else {
      // We let the animation run before hiding it
      const timer = window.setTimeout (() => {
        setShowChildren (false);
      }, duration);
      return () => {
        clearTimeout (timer);
      };
    }
  });

  const className = `fade $ {visible? "": "out"} `.trim ();
  return 
{showChildren && children}
; }

This solves the problem of the child remaining present in the virtual DOM but a

void will remain present in the DOM. You can also choose not to make this

if the element is no longer visible but then the appearance effect is lost.

To manage the appearance effect, you must create the element with the class bland and out then ask the browser to remove the class after the first repaint.
And this is where things get complicated! It would be very easy to manage things in an imperative way.

element.classList.add ("fade");
element.classList.add ("out");
element.offsetHeight; // We force repaint
element.classList.remove ("out"); // Fade in!

In order to simplify the logic we will use the principle of state machines and create 4 states:

const VISIBLE = 1; // The element is visible
const HIDDEN = 2; // The element is hidden
const ENTERING = 3; // The element is animated as input
const LEAVING = 4; // The element is animated on output

And then we'll use them to find out which class to apply. We will also use a ref to memorize the state of the child at the time of its removal.

export function Fade ({
  visible,
  children,
  duration = 300,
  animateEnter = false,
}) {
  const childRef = useRef (children);
  const (state, setState) = useState (
    visible? (animateEnter? ENTERING: VISIBLE): HIDDEN
  );

  if (visible) {
    childRef.current = children;
  }

  useEffect (() => {
    if (! visible) {
      setState (LEAVING);
    } else {
      setState ((s) => (s === HIDDEN? ENTERING: VISIBLE));
    }
  }, (visible));

  useEffect (() => {
    if (state === LEAVING) {
      const timer = setTimeout (() => {
        setState (HIDDEN);
      }, duration);
      return () => {
        clearTimeout (timer);
      };
    } else if (state === ENTERING) {
      document.body.offsetHeight; // force repaint
      setState (VISIBLE);
    }
  }, (state));

  if (state === HIDDEN) {
    return null;
  }

  let className = "fade out";
  if (state === VISIBLE) {
    className = "fade";
  }

  return 
{childRef.current}
; }

Use the style attribute

In order not to multiply the CSS rules, we can generate the style on the fly.

import React, {useEffect, useRef, useState} from "react";

const VISIBLE = 1;
const HIDDEN = 2;
const ENTERING = 3;
const LEAVING = 4;

/ **
 * @param {boolean} visible
 * @param {React.ReactNode} children
 * @param {number} duration in ms
 * @param {boolean} animateEnter Animates the arrival of the element
 * @param {{opacity ?: number, x ?: number, y ?: number, z ?: number}} from
 ** /
export function Fade ({
  visible,
  children,
  duration = 300,
  animateEnter = false,
  from = {opacity: 0},
}) {
  const childRef = useRef (children);
  const (state, setState) = useState (
    visible? (animateEnter? ENTERING: VISIBLE): HIDDEN
  );

  if (visible) {
    childRef.current = children;
  }

  useEffect (() => {
    if (! visible) {
      setState (LEAVING);
    } else {
      setState ((s) => (s === HIDDEN? ENTERING: VISIBLE));
    }
  }, (visible));

  useEffect (() => {
    if (state === LEAVING) {
      const timer = setTimeout (() => {
        setState (HIDDEN);
      }, duration);
      return () => {
        clearTimeout (timer);
      };
    } else if (state === ENTERING) {
      document.body.offsetHeight;
      setState (VISIBLE);
    }
  }, (state));

  if (state === HIDDEN) {
    return null;
  }

  let style = {
    transitionDuration: `$ {duration} ms`,
    transitionProperty: "opacity transform",
  };
  if (state! == VISIBLE) {
    if (from.opacity! == undefined) {
      style.opacity = from.opacity;
    }
    style.transform = `translate3d ($ {from.x ?? 0} px, $ {from.y ?? 0} px, $ {
      from.z ?? 0
    } px) `;
  }

  return 
{childRef.current}
; }

And There you go ! You can use your component to animate your elements.


    

For further

We could push this approach further by automatically detecting the disappearance of a child component. This involves adding more logic to remember the set of rendered child components and their state.