React Transition Group でCSSアニメーション

学習用に[React Transition Group](https://reactcommunity.org/react-transition-group/)の内容を簡単にまとめなおしたものです。
全ての項目を網羅してはいません。

ソースはこちらです。
[react-transition-group-sample](https://github.com/BeeBow6/react-transition-group-sample)
[デモサイト](https://beebow6.github.io/react-transition-group-sample/dist/)

# React Transition Group の概要
ReactでCSSアニメーションを扱う為のライブラリです。
具体的にフェードインやスライドインなどのアニメーションそのものは提供していません。
アニメーションの為のスタイルは自分で用意します。
その用意したスタイルを適切なタイミングでDOMに適用する為の、管理手法を提供しています。

## インストール

```sh
# npm
npm install react-transition-group

#yarn
yarn add react-transition-group
```

# Transition コンポーネント
基本のコンポーネントです。
`props`の変化や時間の経過に応じて、子コンポーネントを再レンダリングします。

## 完成図
ボタンまたはカード本体のクリックで反転アニメーションを開始します。
アニメーション終了時に文字を変更します。
![transition.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/390c55dd-55ad-a0e6-8e9b-3a59050d5b9f.gif)

## 実装方法
`Transition`コンポーネントを用います。

```jsx
import { Transition } from 'react-transition-group';

// カードコンポーネントに適用するStyle
const FLIP_STYLE = {
  // 前面⇒背面
  entering: {
    transition: 'all .5s ease',
    transform: 'perspective(25rem) rotateY(-180deg)'
  },
  // 背面
  entered: {
    transition: '',
    transform: 'perspective(25rem) rotateY(-180deg)'
  },
  // 背面⇒前面
  exiting: {
    transition: 'all .5s ease',
    transform: 'perspective(25rem) rotateY(-360deg)'
  },
  // 前面
  exited: {
    transition: '',
    transform: 'perspective(25rem) rotateY(0)'
  }
};

// 反転するカードコンポーネント
const FlipCard = ({
  flip,
  flipToFront,
  flipToBack
}) => {

  const [text1, setText1] = useState('Moi!');
  const [text2, setText2] = useState('Hei!');

  const callBacks = {
    onEnter: () => setText2('Hei!'),
    onEntered: () => setText2('HeiHei!'),
    onExit: () => setText1('Moi!'),
    onExited: () => setText1('MoiMoi!')
  };

  // 「in」がアニメーションのトリガーとなる
  return (
    <Transition
      in={flip}
      timeout={550}
      {...callBacks}
    >
      {state => (
        <div className="flip-card" style={FLIP_STYLE[state]}>
          <div
            className="flip-card__front"
            onClick={flipToBack}
          >
            {text1}
          </div>
          <div
            className="flip-card__back"
            onClick={flipToFront}
          >
            {text2}
          </div>
        </div>
      )}
    </Transition>
  );
};

const TransitionPage = () => {
  const [flip, setFlip] = useState(false);

  return (
    <div>
      <FlipCard
        flip={flip}
        flipToBack={() => setFlip(true)}
        flipToFront={() => setFlip(false)}
      />
      <button
        className="btn"
        onClick={() => setFlip(!flip)}
      >
        Flip to {flip ? 'front' : 'back'}
      </button>
    </div>
  );
};
```

`Transition`コンポーネントには`children`として、Styleを適用したいコンポーネントを返すコールバック関数を渡します。
この関数は、`props`の`in (Boolean)`propsの値が変更された時と、それから`timeout (ms)`propsで指定した時間分経過後に呼ばれます。
その際に、各ステータスを示す文字列を引数で受け取ります。

|in|変更時|timeout経過後|
|:--|:--|:--|:--|
|**true**|'entering'|'entered'|
|**false**|'exiting'|'exited'|

上記例では、各ステータスをkeyとして適用したいstyleを定義した連想配列を予め用意し、それぞれのタイミングで適用しています。

また、各タイミングごとに実行されるコールバック関数を指定することも可能です。
引数には子コンポーネントのHTMLElementを受け取ります。

- onEnter: 'entering'適用前
- onEntering: 'entering'適用後
- onEntered: 'entered'適用後
- onExit: 'exiting'適用前
- onExiting: 'exiting'適用後
- onExited: 'exited'適用後

[Transition | React Transition Group](https://reactcommunity.org/react-transition-group/transition)

# CSSTransition コンポーネント
上記`Transition`コンポーネントの機能を内包している拡張版です。
より細かくて複雑な指定が可能になります。

## 完成図
コールバック関数を利用して、3つのコンポーネントが連鎖してアニメーションしています。
![cssTransition.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/0c28a0b9-e191-2ade-38c7-48bf58ae7222.gif)

## 実装方法
`in`と`timeout`の値を基に、子コンポーネントを再レンダリングするところは`Transition`と同様です。
異なるのは、`children`にコールバック関数ではなく、コンポーネントを直接取ります。
そして、各タイミングで子コンポーネントの`class(className)`を書き換えます。

```jsx
import { CSSTransition } from 'react-transition-group';

// 昇降する丸
const Slide = ({ show, ...callBack }) => (
  <CSSTransition
    in={show}
    timeout={400}
    unmountOnExit
    classNames="pop__slide-"
    {...callBack}
  >
    <div className="pop__slide"></div>
  </CSSTransition>
);

// 回転する四角
const Rotate = ({ show, children, ...callBack }) => (
  <CSSTransition
    in={show}
    timeout={300}
    unmountOnExit
    classNames="pop__rotate-"
    {...callBack}
  >
    <div className="pop__rotate">
      {children}
    </div>
  </CSSTransition>
);

// フェードインする枠
const Frame = ({ show, children, ...callBack }) => (
  <CSSTransition
    in={show}
    timeout={200}
    unmountOnExit
    classNames="pop__frame-"
    {...callBack}
  >
    <div className="pop__frame">
      {children}
    </div>
  </CSSTransition>
);

const CSSTransitionPage = () => {
  const [flame, setFlame] = useState(false);
  const [rotate, setRotate] = useState(false);
  const [slide, setSlide] = useState(false);
  const [lock, setLock] = useState(false);

  return (
    <div className="pop">
      <button
        className="btn"
        disabled={lock}
        onClick={() => {
          if (flame) {
            setSlide(false);
            setLock(true);
          } else {
            setFlame(true);
            setLock(true);
          }
        }}
      >
        Click !
      </button>
      <Frame
        show={flame}
        onEntered={() => setRotate(true)}
        onExited={() => setLock(false)}
      >
        <Rotate
          show={rotate}
          onEntered={() => setSlide(true)}
          onExited={() => setFlame(false)}
        >
          <Slide
            show={slide}
            onEntered={() => setLock(false)}
            onExited={() => setRotate(false)}
          />
        </Rotate>
      </Frame>
    </div>
  );
};
```

`CSSTransition`の`props`には、`in`, `timeout`に加えて`classNames`も渡します。(`s`がアルヨ）
この値は、子コンポーネントに適用するCSSクラス名のベースとなります。
各タイミングに応じて、`classNames`の値に<font color="Blue">接尾辞</font>をつけた名前を、子コンポーネントに適用します。

例）`classNames="fade"`を指定した場合

|in|変更直後|'-enter'or'-exit'適用後|timeout経過後|
|:--|:--|:--|:--|:--|
|**true**|fade<font color="Blue">-enter</font>|fade<font color="Blue">-enter-active</font>|fade<font color="Blue">-enter-done</font>|
|**false**|fade<font color="Blue">-exit</font>|fade<font color="Blue">-exit-active</font>|fade<font color="Blue">-exit-done</font>|
その他、マウント時に適用される[-appear-*](https://reactcommunity.org/react-transition-group/transition#Transition-prop-appear)もあります。

フェードアニメーションを適用させたい場合は、下記のようなCSSを準備します。

```css
.fade-enter {
  opacity: 0;
}
.fade-enter-active {
  transition: all .2s ease-in-out;
  opacity: 1;
}
.fade-exit {
  opacity: 1; /* opacityの規定値は1だが、省略不可 */
}
.fade-exit-active {
  transition: all .2s ease-in-out;
  opacity: 0;
}
```

基本的にマウント直後はクラスは適用されていません。アニメーション終了後は`*-done`が適用された状態です。
上記例では、マウント直後と`fade-exit-done`適用後はコンポーネントが見えてます。
`in`propsが`false`の時にコンポーネントをアンマウントにしたい場合は、[`unmountOnExit`](https://reactcommunity.org/react-transition-group/transition#Transition-prop-unmountOnExit)に`true`を指定します。

[CSSTransition | React Transition Group](https://reactcommunity.org/react-transition-group/css-transition)

# TransitionGroup コンポーネント
`Transition`または`CSSTransition`のリストを管理する為のコンポーネントです。

## 完成図
リスト項目追加時と削除時にアニメーションが発生します。
![transitionGroup.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/47f58ee9-c012-acd3-9d90-047fdc8f9081.gif)

## 実装方法
子コンポーネントとなる`CSSTransition`の実装方法はほとんど変わりませんが、トリガーである`in`propsは指定しません。この管理を`TransitionGroup`に任せます。

```jsx
import { TransitionGroup, CSSTransition } from 'react-transition-group';

// リスト項目
const Card = ({ onDelete, children }) => (
  <li className="fade-card">
    <div className="fade-card__body">
      {children}
      <button
        type="button"
        className="fade-card__del"
        onClick={onDelete}
      >
        delete
        </button>
    </div>
  </li>
);

// 項目追加用
const AddList = ({ addItem }) => {
  const [value, setValue] = useState('');
  const itemId = useRef(0);

  return (
    <form
      className="fade-add"
      onSubmit={e => {
        e.preventDefault();
        itemId.current = itemId.current + 1;
        setValue('');
        addItem({
          id: itemId.current,
          value
        });
      }}
    >
      <input
        type="text"
        className="fade-add__text"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button
        type="submit"
        className="btn"
      >
        Add
      </button>
    </form>
  );
};

// リスト管理
const TransitionGroupPage = () => {
  const [list, setList] = useState([]);

  return (
    <div>
      <AddList addItem={item => setList([item, ...list])} />
      <TransitionGroup component="ul">
        {list.map(({ id, value }) => (
          // 「in」は指定しない
          <CSSTransition
            key={id}
            timeout={200}
            classNames="fade-card-"
          >
            <Card onDelete={() => setList(list.filter(item => item.id !== id))}>
              {value}
            </Card>
          </CSSTransition>
        ))}
      </TransitionGroup>
    </div>
  );
};
```

リストの追加削除は、通常のReactコンポーネントでリスト項目を扱う時と同様に、該当の`state`の配列に対して追加or削除するだけです。
`TransitionGroup`によって、項目追加時（マウント時）に`enter`の、削除時（アンマウント時）に`exit`のスタイルが適用されます。
`TransitionGroup`コンポーネントは、実際にHTMLタグがレンダリングされます。規定では`<div>`です。
他のタグに変更したい場合は、`component`の値に任意のタグ名を指定します。
タグの挿入を避けたい場合は、`component={null}`を指定します。

[TransitionGroup | React Transition Group](https://reactcommunity.org/react-transition-group/transition-group)

# ReactRouter との併用
[ReactRouter](https://reacttraining.com/react-router/web/guides/quick-start)でルーティングする際に、React Transition Groupのアニメーションを適用する方法です。
ReactRouterの解説は割愛いたします。下記の記事がとても分かりやすいです。
[react-router@v4を使ってみよう：シンプルなtutorial | Qiita](https://qiita.com/muiscript/items/b4ca1773580317e7112e)

## 完成図
各ページがフェードで切り替わります。
![reactRouter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/75faa5e6-f45c-297f-b538-63fa1979b72e.gif)

## 実装方法
`ReactRouter`の`Route`コンポーネントの`children`に、`CSSTransition`コンポーネントを返す関数を渡します。
この関数はルーティングの度に各コンポーネント毎に呼ばれ、ルーティングに関連する情報を含むオブジェクトを引数で受け取ります。
このオブジェクトの`match`プロパティは、ルートがマッチしているコンポーネントの時のみ値が存在し、その他の場合は`null`となります。
この`match`の値を`in`propsに適用することで、ルーティングに応じて各`CSSTransition`コンポーネントのアニメーションがトリガーされるようになります。
同時に`unmountOnExit`も指定しましょう。

```jsx
import { BrowserRouter, Route, NavLink } from 'react-router-dom';
import { CSSTransition } from 'react-transition-group';
import TransitionPage from './transition';
import CSSTransitionPage from './cssTransition';
import TransitionGroupPage from './transitionGroup';

const ROUTES = [
  { name: 'Transition', path: '/', Component: TransitionPage },
  { name: 'CSSTransition', path: '/css', Component: CSSTransitionPage },
  { name: 'TransitionGroup', path: '/group', Component: TransitionGroupPage }
];

const App = () => {
  return (
    <div className="page">
      <BrowserRouter>
        <ul className="menu">
          {ROUTES.map(({ name, path }) => (
            <li key={path}>
              <NavLink
                to={path}
                className="menu__item"
                activeClassName="menu__item--active"
                exact
              >
                {name}
              </NavLink>
            </li>
          ))}
        </ul>
        <div className="page__container">
          {ROUTES.map(({ path, Component }) => (
            <Route key={path} path={path} exact>
              {({ match }) => (
                <CSSTransition
                  in={match != null}
                  timeout={300}
                  classNames="page__item-"
                  unmountOnExit
                >
                  <div className="page__item">
                    <Component />
                  </div>
                </CSSTransition>
              )}
            </Route>
          ))}
        </div>
      </BrowserRouter>
    </div>
  );
};
```

ルーティング切替時、前後のコンポーネントが同時に存在することになります。
レイアウトによっては、`position:absolute`などを用いて通常フローから切り離すなどの対応を取る必要があります。

[Usage with React Router | React Transition Group](https://reactcommunity.org/react-transition-group/with-react-router)