Animations as React components

Laimonas K - Feb 17 '20 - - Dev Community

Story begins as usual - project is just starting, the design is "almost" done and the requirements are all over the place. Not wanting to deal with with major refactorings later down the road, team decides to follow atomic design pattern as much as possible.
Life is good. All changes are isolated in small chunks, but suddenly, a wild animation for an already developed component appears! styled-components to the rescue!

Animation component

As an example, let's create a simple animation for rotating an item. It is just a simple wrapper, which uses chainable .attrs to pass dynamic props and set animation properties. Note: it should only use css and values, that can be used in transitions. So no px to % transitions.
For passing props, you could also use tagged template literal, but it would create a new classname for each different variant of the transition.

import styled from "styled-components";

const Rotate = styled("div").attrs(
  ({ state, duration = "300ms", start = 0, end = 180 }) => ({
    style: {
      transition: duration,
      transform: `rotate(${state ? start : end}deg)`
    }
  })
)``;

export default Rotate;

Usage

To use it, just import the animation, wrap the component you want to animate and provide some sort of a state handler. In this case it is just a simple component to change the state, when clicking a button. In practice, it could be almost anything from a button click, to a form validation status.

<StateSwitcher>
  {({ state }) => (
    <Rotate state={state} duration="1s" end={360}>
      <Element>Rotate</Element>
    </Rotate>
  )}
</StateSwitcher>

Combining multiple animations

Rinse and repeat. The setup is almost identical.

import styled from "styled-components";

const Opacity = styled("div").attrs(
  ({ state, duration = "300ms", start = 0, end = 1 }) => ({
    style: {
      transition: duration,
      opacity: state ? end : start
    }
  })
)``;

export default Opacity;

Now use it to wrap and voila.

<StateSwitcher>
  {({ state }) => (
    <Opacity state={state}>
      <Rotate state={state}>
        <Element>Rotate + Opacity</Element>
      </Rotate>
    </Opacity>
  )}
</StateSwitcher>

Testing

Testing this setup is dead simple with @testing-library/react. Just change the state and check what the resulting style changes.

import React from "react";
import { render } from "@testing-library/react";

import Rotate from "./Rotate";

describe("Rotate", () => {
  it("renders Rotate and changes state ", async () => {
    const component = state => (
      <Rotate state={state} start={0} end={123} data-testid="rotate-transition">
        <div>COMPONENT</div>
      </Rotate>
    );

    const { rerender, getByTestId } = render(component(true));
    const RenderedComponent = getByTestId("rotate-transition");
    let style = window.getComputedStyle(RenderedComponent);

    expect(style.transform).toBe("rotate(0deg)");
    rerender(component(false));

    style = window.getComputedStyle(RenderedComponent);

    expect(style.transform).toBe("rotate(123deg)");
  });
});

Results

You could have many different variants (move, rotate, color ...) and extend these much more - handle animation finish callbacks, setTimeouts and etc.

This setup might not be the suitable in all cases, but in my case, it ticks all the right marks:

  • Easy to use and share;
  • Easy to extend;
  • Easy to test;

. . .
Terabox Video Player