LoginSignup
98
78

More than 3 years have passed since last update.

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

Posted at

学習用にReact Transition Groupの内容を簡単にまとめなおしたものです。
全ての項目を網羅してはいません。

ソースはこちらです。
react-transition-group-sample
デモサイト

React Transition Group の概要

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

インストール

# npm
npm install react-transition-group

#yarn
yarn add react-transition-group

Transition コンポーネント

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

完成図

ボタンまたはカード本体のクリックで反転アニメーションを開始します。
アニメーション終了時に文字を変更します。
transition.gif

実装方法

Transitionコンポーネントを用います。

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を適用したいコンポーネントを返すコールバック関数を渡します。
この関数は、propsin (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

CSSTransition コンポーネント

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

完成図

コールバック関数を利用して、3つのコンポーネントが連鎖してアニメーションしています。
cssTransition.gif

実装方法

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

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>
  );
};

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

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

in 変更直後 '-enter'or'-exit'適用後 timeout経過後
true fade-enter fade-enter-active fade-enter-done
false fade-exit fade-exit-active fade-exit-done

その他、マウント時に適用される-appear-*もあります。

フェードアニメーションを適用させたい場合は、下記のような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適用後はコンポーネントが見えてます。
inpropsがfalseの時にコンポーネントをアンマウントにしたい場合は、unmountOnExittrueを指定します。

CSSTransition | React Transition Group

TransitionGroup コンポーネント

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

完成図

リスト項目追加時と削除時にアニメーションが発生します。
transitionGroup.gif

実装方法

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

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

ReactRouter との併用

ReactRouterでルーティングする際に、React Transition Groupのアニメーションを適用する方法です。
ReactRouterの解説は割愛いたします。下記の記事がとても分かりやすいです。
react-router@v4を使ってみよう:シンプルなtutorial | Qiita

完成図

各ページがフェードで切り替わります。
reactRouter.gif

実装方法

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

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

98
78
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
98
78