Animating Content Between Components in React
Tags: react, css, animation, learning
Published: 2021-01-04 14:53:37

I've been working on a game app in React with some fellow students in Flatiron's Software Engineering Bootcamp and found myself in my least favorite place; staring down the barrel of a complex CSS challenge. I wanted to animate pieces around the game board after a player made a move. After a fair bit of googling React animation libraries [(This is a great source for animation options)](https://medium.com/hackernoon/5-ways-to-animate-a-reactjs-app-in-2019-56eb9af6e3bf), I decided that my use case combined with my desire to make peace with CSS meant I was going the full CSS route. This was no simple task for me so I thought I'd share what I learned. 

##TL;DR
The high-level view of my approach is to have resources animate into new components when the user clicks on them. To achieve this I have the resources render in the new component with a state change and force them back into their original position with CSS, then animate them into their new position. If you just want to get down to the code and see the results, you can check out the [code pen](https://codepen.io/welytle/pen/RwGMydL?editors=0110) or see the full code at the end of the article.

## Set Up
I pared down the problem as best I could to make it bite-sized. Here is the basic boilerplate code of the pre-animated situation. 
```jsx
//App.js
const FarmFig = ({ image, handleClick=null, setRef }) => {
  return (
      <img className="image" src={image} ref={setRef} onClick={handleClick} />
  );
};

class App extends React.Component {
  constructor(){
    super()
           this.center = React.createRef();
  this.state = {
    this.state = {
      images: {
        tl:
          "https://lh3.googleusercontent.com/proxy/YSIR4H4fU2Tf5vmbmeixy6m6ZcTXvS9wEo8q4gxOiqEg8XXPod1ZaGJbc8-wngYJwkR6QHEfjvO3w4QogZJqVH5nJjhJaMk",
        c:
          "https://lh3.googleusercontent.com/proxy/29-YDS42UPIZNuPicKnpkmh2sw_th3Sa41d6iiGT8XH1vXfjfpNgUCK1-CxlMlT40eaJP25ylJ8IRUiCEBwTfIyuBB8izJ8",
        br:
          "https://pngarchive.com/public/uploads/small/11559054782q4dsokodad1svijk1zzlyrjdwtmlygkkaxxvooeqevdyrbomu3b5vwcwst0ndcidr89gdf0nyleyffoncphgazeqmnpmdubfypow.png",
      },
  }
} 
handleClick = ({target}) => {
  this.setState(prevState => {
      //switch clicked image with the center image
         if (prevState.images.tl === target.src) {
           prevState.images.tl = prevState.images.c;
           prevState.images.c = target.src
         } else {
           prevState.images.br = prevState.images.c;
           prevState.images.c = target.src
         }
         return {images: prevState.images}
       })
     }

          render() {
            const{tl, c, br} = this.state.images
            return (
              <div className="container">
                <div className="top-left">
                  <FarmFig image={tl} handleClick={this.handleClick}/>
                </div>
                <div className="container">
                  <FarmFig image={c} setRef={this.center} />
                </div>
                <div className="bot-right">
                  <FarmFig image={br} handleClick={this.handleClick} />
                </div>
              </div>
            )
          }
        }
```
```css
/* css */
.container {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  justify-content: center;
  height: 90vh;
  width: 100%;
}
.top-left {
  align-self: flex-start;
  transform: rotate(180deg)
}
.bot-right {
  align-self: flex-end;
}
.image {
  width: 175px;
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/frqu5o2f1a8maohey5ak.gif)
So far, pretty basic set up. We have three images: top-left, center, and bottom-right. When we click on either the top-left or bottom-right images we trigger a state change that swaps the clicked and center images. We also included a `ref` to the center image, we'll use that soon. Now to animate this transition.

## Adding animation
In order to animate the images, we first need to add a few items to state.

```jsx
this.state = {
      images: {
        tl:
          "https://lh3.googleusercontent.com/proxy/YSIR4H4fU2Tf5vmbmeixy6m6ZcTXvS9wEo8q4gxOiqEg8XXPod1ZaGJbc8-wngYJwkR6QHEfjvO3w4QogZJqVH5nJjhJaMk",
        c:
          "https://lh3.googleusercontent.com/proxy/29-YDS42UPIZNuPicKnpkmh2sw_th3Sa41d6iiGT8XH1vXfjfpNgUCK1-CxlMlT40eaJP25ylJ8IRUiCEBwTfIyuBB8izJ8",
        br:
          "https://pngarchive.com/public/uploads/small/11559054782q4dsokodad1svijk1zzlyrjdwtmlygkkaxxvooeqevdyrbomu3b5vwcwst0ndcidr89gdf0nyleyffoncphgazeqmnpmdubfypow.png",
      },
      animate: true,
      transition: {
        center: {
          startTop: 0,
          startRight: 0,
        },
        corner: {
          farmItem: null,
          startTop: 0,
          startRight: 0,
        },
      },
    };
```
We've added a `transition` object to keep track of the **adjusted** starting position of the images after they re-render. Remember, we are going to render the swapped images in their new components and move them to look like they are still in their original positions then animate them to their new location.  

Next, we need to calculate these adjusted starting positions after we click on an image. This will be done in our `handlClick` function.

```jsx
 handleClick = ({ target }) => {
    // find location of clicked image
    const imageStartTop =
      target.getBoundingClientRect().top + document.documentElement.scrollTop;

    const imageStartRight =
      target.getBoundingClientRect().right +
      document.documentElement.scrollLeft;

    //find ending location of clicked image
    let endLoc = this.getCenterLoc();
    let selectedImage;
    this.setState((prevState) => {
      if (prevState.images.tl === target.src) {
        // Swap the selected and center images
        selectedImage = "tl";
        prevState.images.tl = prevState.images.c;
        prevState.images.c = target.src;
      } else {
        selectedImage = "br";
        prevState.images.br = prevState.images.c;
        prevState.images.c = target.src;
      }
      return {
        images: prevState.images,
        // We set animate to false to temporarily to allow images to relocate
        animate: false,
        transition: {
          center: {
            // y distance in px the center image needs to move
            startTop: imageStartTop - endLoc[0],
            // x distance in px the center image needs to move
            startRight: imageStartRight - endLoc[1],
          },
          corner: {
            farmItem: selectedImage,
            // y distance in px the clicked image needs to move
            startTop: endLoc[0] - imageStartTop,
            // y distance in px the clicked image needs to move
            startRight: endLoc[1] - imageStartRight,
          },
        },
      };
    });
     // Wait briefly then change the animation flag to trigger a re-render and the animation. 
    setTimeout(() => this.setState({ animate: true }), 200);
  };
```
where: 
```jsx
getCenterLoc = () => {
    const imageEndTop =
      this.center.current.getBoundingClientRect().top +
      document.documentElement.scrollTop;

    const imageEndRight =
      this.center.current.getBoundingClientRect().right +
      document.documentElement.scrollLeft;
    return [imageEndTop, imageEndRight];
  };
```
There's a lot going on here, but at the end of the day, it's just a cumbersome subtraction problem. We use the `ref` on the center image and the event target of the click to get the position of clicked and center image relative to the viewport. `element.getBoundingClientRect()` gives the coordinates of `element` relative to the document and adding `document.documentElement.scrollTop` accounts for any offset in the document if the page is scrolled. Then we set our state with the *difference* between the center image and the selected image.

After this, we use a `setTimeout` call to wait briefly before changing the `animate` flag in state and triggering the animation.

Now when we need to apply these new locations to the appropriate images when they are clicked. In our render method, we will add a ternary checking our `this.state.animate` property

```jsx
const animClass = this.state.animate ? "force-move" : "";
```
Then we can pass this new class, along with our adjusted locations down into the appropriate `FarmFig` components, and make the following updates to `FarmFig` and to our CSS file.

```jsx
const FarmFig = ({ image, handleClick = null, setRef, locs, animClass }) => {
  return (
        <img
          className={`image ${animClass}`}
          src={image}
          ref={setRef}
          onClick={handleClick}
          style={{
          transform: `translate(${locs[1]}px, ${locs[0]}px)`,
        }}
        />
  );
};
```
```css
.force-move {
    transition: transform 1s;
    transition-delay: 1s;
    transform: translate(0, 0) !important;
  }
```
Whew... We finally have some moving animations!!!!
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/x1gsz5lfyde6juogursv.gif)

This is great, except... I originally needed animations to move pieces **around** a space, not through it. Unfortunately, CSS doesn't support putting delays between multiple types of `transforms` on the same object. My solution to this is to add some bonus `divs` around the `FarmFig` `img` and transform each of those with timed delays (This definitely feels a bit hacky, anyone have a better idea?) 

Finally updating our `FarmFig` and CSS files accordingly:

```jsx
<div
      className={`${animClass}X`}
      style={{
        transform: `translate(${locs[1]}px`,
      }}
    >
      <div
        className={`${animClass}Y`}
        style={{
          transform: `translateY(${locs[0]}px)`,
        }}
      >
        <img
          className={`image`}
          src={image}
          ref={setRef}
          onClick={handleClick}
        />
...
```
```css
.force-moveX {
    transition: transform 1s;
    transform: translate(0) !important;
  }
  .force-moveY {
    transition: transform 1s;
    transition-delay: 1s;
    transform: translateY(0) !important;
  }
```

and voila! 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w0u39n9xhh2e02i046nh.gif)

## Wrap-up
Building out custom animations in React using just CSS is undeniably cumbersome and requires a fair bit of code. It is certainly not for every situation. However, if you want to limit how many libraries your project relies on, have a unique use case, or have decided it's finally your time to face CSS it can certainly be done. Hope this is helpful, let me know if I totally screwed something up! 

## Full Code

### CodePen
{% codepen https://codepen.io/welytle/pen/RwGMydL %}

### Code
```jsx
// App.js
const FarmFig = ({ image, handleClick = null, setRef, locs, animClass }) => {
  return (
    <div
      className={`${animClass}X`}
      style={{
        transform: `translate(${locs[1]}px`,
      }}
    >
      <div
        className={`${animClass}Y`}
        style={{
          transform: `translateY(${locs[0]}px)`,
        }}
      >
        <img
          className={`image`}
          src={image}
          ref={setRef}
          onClick={handleClick}
        />
      </div>
    </div>
  );
};

class App extends React.Component {
  constructor() {
    super();
    this.center = React.createRef();
    this.state = {
      images: {
        tl:
          "https://lh3.googleusercontent.com/proxy/YSIR4H4fU2Tf5vmbmeixy6m6ZcTXvS9wEo8q4gxOiqEg8XXPod1ZaGJbc8-wngYJwkR6QHEfjvO3w4QogZJqVH5nJjhJaMk",
        c:
          "https://lh3.googleusercontent.com/proxy/29-YDS42UPIZNuPicKnpkmh2sw_th3Sa41d6iiGT8XH1vXfjfpNgUCK1-CxlMlT40eaJP25ylJ8IRUiCEBwTfIyuBB8izJ8",
        br:
          "https://pngarchive.com/public/uploads/small/11559054782q4dsokodad1svijk1zzlyrjdwtmlygkkaxxvooeqevdyrbomu3b5vwcwst0ndcidr89gdf0nyleyffoncphgazeqmnpmdubfypow.png",
      },
      animate: true,
      transition: {
        center: {
          startTop: 0,
          startRight: 0,
        },
        corner: {
          farmItem: null,
          startTop: 0,
          startRight: 0,
        },
      },
    };
  }
  getCenterLoc = () => {
    const imageEndTop =
      this.center.current.getBoundingClientRect().top +
      document.documentElement.scrollTop;

    const imageEndRight =
      this.center.current.getBoundingClientRect().right +
      document.documentElement.scrollLeft;
    return [imageEndTop, imageEndRight];
  };

  handleClick = ({ target }) => {
    // find location of clicked image
    const imageStartTop =
      target.getBoundingClientRect().top + document.documentElement.scrollTop;

    const imageStartRight =
      target.getBoundingClientRect().right +
      document.documentElement.scrollLeft;

    //find location of ending location
    let endLoc = this.getCenterLoc();
    let selectedImage;
    this.setState((prevState) => {
      if (prevState.images.tl === target.src) {
        selectedImage = "tl";
        prevState.images.tl = prevState.images.c;
        prevState.images.c = target.src;
      } else {
        selectedImage = "br";
        prevState.images.br = prevState.images.c;
        prevState.images.c = target.src;
      }
      return {
        images: prevState.images,
        animate: false,
        transition: {
          center: {
            startTop: imageStartTop - endLoc[0],
            startRight: imageStartRight - endLoc[1],
          },
          corner: {
            farmItem: selectedImage,
            startTop: endLoc[0] - imageStartTop,
            startRight: endLoc[1] - imageStartRight,
          },
        },
      };
    });
    setTimeout(() => this.triggerAnim(), 200);
  };

  triggerAnim = () => {
    this.setState({ animate: true });
  };

  getOldLoc = (loc) => {
    const { corner } = this.state.transition;
    let top, right;
    if (corner.farmItem === loc) {
      top = corner.startTop;
      right = corner.startRight;
    } else {
      top = 0;
      right = 0;
    }
    return [top, right];
  };

  render() {
    const { tl, c, br } = this.state.images;
    const { center } = this.state.transition;
    const animClass = this.state.animate ? "force-move" : "";
    return (
      <div className="container">
        <div className="top-left">
          <FarmFig
            image={tl}
            handleClick={this.handleClick}
            locs={this.getOldLoc("tl")}
            animClass={animClass}
          />
        </div>
        <div className="center">
          <FarmFig
            image={c}
            setRef={this.center}
            locs={[center.startTop, center.startRight]}
            animClass={animClass}
          />
        </div>
        <div className="bot-right">
          <FarmFig
            image={br}
            handleClick={this.handleClick}
            locs={this.getOldLoc("br")}
            animClass={animClass}
          />
        </div>
      </div>
    );
  }
}
```
```css
\* css *\
.container {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    height: 90vh;
    width: 100%;
  }
  .center {
    align-self: center;
  }
  .top-left {
    align-self: flex-start;
  }
  .bot-right {
    align-self: flex-end;
  }
  .image {
    width: 150px;
    height: 130px;
  }
  
  .force-moveX {
    transition: transform 1s;
    transform: translate(0) !important;
  }
  .force-moveY {
    transition: transform 1s;
    transition-delay: 1.5s;
    transform: translateY(0) !important;
  }
```