Help us understand the problem. What is going on with this article?

react-transition-group

react-transition-group

react-transition-groupドキュメント

react-transition-groupはReactでCSSアニメーションを扱う為のライブラリです。
見栄えのするモーション自体を提供してくれるわけではなく、CSSを適用するタイミングを提供してくれるので、自分でアニメーションのCSSを書いてアニメーションさせます。

下記の4つのコンポーネントが提供されます。

  • Transition
  • CSSTransition
  • SwitchTransition
  • TransitionGroup

今回はcreate-react-appを使い、CSS(SCSS)はCSSModulesを使っていきます。
※レンダリング回数削減などパフォーマンス面については扱いません。

インストール

terminal
//create-react-appのインストール
npx create-react-app プロジェクト名

//プロジェクト直下に移動
cd プロジェクト名

//node-sassのインストール(scssではなくcssを使う場合は不要です。)
npm i -D node-sass

//react-transition-groupのインストール
npm i -S react-transition-group

//起動
npm start

src/App.jsの不要な部分を削除します。

src/App/js
import React from "react";
import "./App.css";

function App() {
  return (
    <div className="App">

  {/* ここにこれから作るコンポーネントを配置 */}

    </div>
  );
}

export default App;

Transition

シンプルなトランジション

どのタイミングでinと状態が変化しているか視覚化しています。

transition.gif

src\components\singleTransition\SingleTransition.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./singleTransition.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
   exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};

//SingleTransitionコンポーネント
const SingleTransition = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (

    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>

          <Transition in={mount} timeout={1000} >

            {(state) =>
              <div className={Style.circleShape} style={transitionStyle[state]} >
                <div>
                  <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                  <p className={Style.circleText}> {state}</p>
              </div>
             </div>}

          </Transition>

        </div>

      </div>

    </div>

  );

}

export default SingleTransition;
src\components\singleTransition\singleTransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import SingleTransition from "./components/singleTransition/SingleTransition";

function App() {
  return (
    <div className="App">
     <ChainTransition />
    </div>
  );
}

export default App;

classNameでstateを含むクラス名を指定することで、transitionの状態によって独自のクラスを適用する事もできます

必須のProps

in

inの状態 結果
inがtrueになる マウント開始
inがfalseになる アンマウント開始

timeout

entering、exitingのトランジションを使う場合で、addEndListenerを設定しない場合は必須です。

timeoutの指定による状態の変化
状態 初期 timeoutで指定した時間経過後
マウント時 entering entered
アンマウント時 exiting exited

各transitionに個別にタイムアウトを指定することもできます。

sample
timeout={{
  appear: 500,
  enter: 300,
  exit: 500,
}}

addEndListener

entering、exitingのトランジションを使う場合、timeoutを設定しない場合は必須です。
カスタムのtransition終了トリガーを追加して、動的にtimeout時間を設定したい場合に使用します。

sample
<Transition in={mount} {...callBacks} timeout={1000} >
//↓
<Transition in={mount} {...callBacks} addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}>

inがtrueになる度に追加されるので、毎回doneを呼ぶタイミングでイベントの解除が必要です。

timeoutとaddEndListener

timeoutoとaddEndListenerをどちらも指定しないとコンポーネントのトランジションはenteredとexitedだけを繰り返します。
両方指定した場合はaddEndListenerはフォールバックとして使用されます。

4つの状態

inとtimeoutの組み合わせで、コンポーネントに4つの状態が提供されます。
使用しないものについては、省略可能です。

  • entering
  • enterd
  • exiting
  • exited

enteringとexitingのtransitionの時間の長さについては、通常コンポーネントのtimeoutの値とそろえますが、あえて違う値にすることもできます。

inとtimeout以外のProps

otherprops.gif

src\components\otherProps\OtherProps.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./otherProps.module.scss";

//アニメーションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
  exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};


const OtherProps = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (
    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>
          <p>Normal</p>
          <Transition in={mount} timeout={1000} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>mountOnEnter</p>
          <Transition in={mount} timeout={1000} mountOnEnter >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>unmountOnExit</p>
          <Transition in={mount} timeout={1000} unmountOnExit >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
              </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>enter=false</p>
          <Transition in={mount} timeout={1000} enter={false} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>exit=false</p>
          <Transition in={mount} timeout={1000} exit={false} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>nodeRef</p>
          <Transition in={mount} timeout={1000} nodeRef >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

      </div>

    </div>

  );

}

export default OtherProps;
src\components\otherProps\otherProps.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import OtherProps from "./components/otherProps/OtherProps";

function App() {
  return (
    <div className="App">

     <ChainTransition />
    </div>
  );
}

export default App;

in、timeout以外のProps

Props 未指定時(暗黙的に指定されている) 未指定時から変更する場合 変更時内容
enter enter、enter={true} enter={false} enteringにならない
exit exit、exit={true} exit={false} exitingにならない
mountOnEnter mountOnEnter={false} mountOnEnter、mountOnEnter={true} 遅延マウント(初回のみ)
unmountOnExit unmountOnExit={false} unmountOnExit、unmountOnExit={true} exitedでアンマウント
nodRef nodeRef={false} nodeRef、nodeRef={true} entering、exitingにならない
appear appear={false} appear={true} appearの動作(in=trueと一緒に指定)

appear

各コンポーネントのinの初期値をtrue、appear=trueにします。

変更箇所のみ

src\components\otherProps\OtherProps.js
//mountの初期値をtrueにして、in=trueにする
const [mount, setMount] = useState(true);

//各コンポーネントにappear={true}を追加
<Transition in={mount} timeout={1000} {...callBacksNormal} appear={true} >

コールバック

状態の変化時に処理を行う事ができます。

sample
//状態変化時のコールバック
const callBacks = {
  onEnter: () => console.log("enterです"),
  onEntered: () => console.("enteredです"),
  onExit: () => console.log("exitです"),
  onExited: () => console.log("exitedです"),
};


//Transitionに{...callBacks}を追加
<Transition in={mount} {...callBacks} timeout={1000} >

Transitionを連鎖させる

chain.gif

src\components\chainTransition\ChainTransition.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./chaintransition.module.scss";

//アニメーションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
  exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};

//ChainTransitionコンポーネント
const ChainTransition = () => {

  //マウントの状態を管理
  const [firstCircle, setFirstCircle] = useState(false);
  const [secondCircle, setSecondCircle] = useState(false);
  const [thirdCircle, setThirdCircle] = useState(false);
  const [fourthCircle, setFourthCircle] = useState(false);
  const [fifthCircle, setFifthCircle] = useState(true);
  const [sixthCircle, setSixthCircle] = useState(true);
  const [seventhCircle, setSeventhCircle] = useState(true);

  //マウントのオンオフを切り替える
  const changer = () => {
    setFirstCircle(!firstCircle);
  };

  const callBacks = {

    onEnter: () => {
      setSecondCircle(true);
    },
    onEntering: () =>{
      setThirdCircle(true);
    },
    onEntered: () => {
      setFourthCircle(true);
    },
    onExit: () => {
      setFifthCircle(false);
    },
    onExiting: () => {
      setSixthCircle(false);
    },
    onExited: () => {
      setSeventhCircle(false);
    },

  };

  return (
    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>
        <p>(trigger)</p>
        <Transition in={firstCircle} timeout={1000} {...callBacks}>
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {firstCircle ? "in=true" : "in=false"}</p>
              <p className={Style.circleText}> {state}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEnter</p>
        <Transition in={secondCircle} timeout={1000}  >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {secondCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEntering</p>
        <Transition in={thirdCircle} timeout={1000}>
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {thirdCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEntered</p>
        <Transition in={fourthCircle} timeout={1000} >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {fourthCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onExit</p>
        <Transition in={fifthCircle} timeout={1000}  >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {fifthCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onExiting</p>
        <Transition in={fifthCircle} timeout={1000} >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {sixthCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>


      <div className={Style.circleMember}>
        <p>onExited</p>
          <Transition in={seventhCircle} timeout={1000} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {seventhCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
          </Transition>
      </div>

    </div>

  </div>

  );
}

export default ChainTransition;
src\components\chainTransition\chaintransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  color: red;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import ChainTransition from "./components/chainTransition/ChainTransition";

function App() {
  return (
    <div className="App">

     <ChainTransition />
    </div>
  );
}

export default App;

コールバックのタイミング

nodeRef Propが渡されると、コールバックを使ってもnodeは渡されません。

コールバックの種類 適用タイミング
onEnter entering適用前
onEntering entering適用時
onEntered entered適用時
onExit exiting適用前
onExiting exiting適用時
onExited exited適用時

CSSTransition

Transitionとの大きな違いは、親要素の名前がCSSTransitionになっていることと、CSSTransition用のPropsとしてclassNameが使える事です。

csstransition.gif

src\components\singleCSSTransition\SingleCSSTransition.js
import React, { useState } from "react";
import { CSSTransition } from "react-transition-group";
import Style from "./singleCSSTransition.module.scss";


//SingleCSSTransitionコンポーネント
const SingleCSSTransition = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (

    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup} >

        <div className={Style.circleMember} >

          <CSSTransition
            in={mount}
            timeout={1000}
            classNames={{
              appear:Style.testAppear,
              appearActive:Style.testAppearActive,
              appearDone:Style.testAppearDone,
              enter:Style.testEnter,
              enterActive: Style.testEnterActive,
              enterDone:Style.testEnterDone,
              exit:Style.testExit,
              exitActive: Style.testExitActive,
              exitDone: Style.testExitDone,
            }}
            >

            {(state) =>
              <div className={Style.circleShape} >
                <div>
                  <p className={Style.circleText} > {mount ? "in=true" : "in=false"}</p>
                  <p className={Style.circleText} > {state}</p>

              </div>
             </div>}

          </CSSTransition>

        </div>

      </div>

    </div>

  );

}

export default SingleCSSTransition;
src\components\singleCSSTransition\singleCSSTransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}

//apperaの最初のフレーム瞬間の状態
.testAppear {
  transition: all 1s ease;
  border-radius: 10px;
  transform: rotateZ(30deg) scale(2);
  opacity: 0;
}

//.testAppearの直後の状態
.testAppearActive {
}

//appearの最終フレームの状態
.testAppearDone {
  transition: all 1s ease;
  border-radius: 50px;
  opacity: 1;
}

//enter中の最初のフレーム瞬間の状態
.testEnter {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: red;
}
//enter中の最終フレームの状態
.testEnterActive {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: red;
}

//enter完了の状態
.testEnterDone {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: green;
}

//exitの初期状態
.testExit {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: green;
}

//exit中の最終フレームの状態
.testExitActive {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: blue;
}
//exit完了
.testExitDone {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: gray;
}
src\App.js
import React from "react";

import SingleCSSTransition from "./components/singleCSSTransition/SingleCSSTransition";

function App() {
  return (
    <div className="App">

     <SingleCSSTransition />
    </div>
  );
}

export default App;

className

CSSTransitionのclassName Propsでは、各状態にクラス名を付けられます。

sample
<CSSTransition
  classNames={{
    enter:Style.testEnter,
    enterActive: Style.testEnterActive,
    enterDone:Style.testEnterDone,
    exit:Style.testExit,
    exitActive: Style.testExitActive,
    exitDone: Style.testExitDone,
}}>  

今回はCSSModulesを採用しているので、使用する各状態用用のクラスに個別に名前をつける必要がありますが、他の方法でCSSを指定している場合接頭辞を指定すれば、自動的にクラス名が生成されます。
※上記のように任意のクラス名に変更することも可能です。

table:接頭辞をtestにした場合

- active done
test-appear test-appear-active test-appear-done
test-enter test-enter-active test-enter-done
test-enter test-exit-active test-exit-done

※appearクラスの追加タイミングを追ってみると、enterクラスと同時に適用されてしまうので使い方には注意が必要です。

appear

コンポーネントのinの初期値をtrue、appear=trueにします。

変更箇所のみ記載

src\components\singleCSSTransition\SingleCSSTransition.js
//mountの初期値をtrueにしてin=trueにする
const [mount, setMount] = useState(true);

//CSSTransitonにappear={true}を追加
<CSSTransition
  in={mount}
  {...callBacks}
  timeout={1000}
  classNames={{
    appear:Style.testAppear,
    appearActive:Style.testAppearActive,
    appearDone:Style.testAppearDone,
    enter:Style.testEnter,
    enterActive: Style.testEnterActive,
    enterDone:Style.testEnterDone,
    exit:Style.testExit,
    exitActive: Style.testExitActive,
    exitDone: Style.testExitDone,
  }}
  appear={true}
>

SwitchTransition

SwitchTransitionでTransition、CSSTransitionを囲みます。
SwitchTransition独自のPropsとしてmodeがあります。

switch.gif

src\components\transitionSwitching\TransitionSwiching.js
import React, { useState } from "react";
import { SwitchTransition, Transition } from "react-transition-group";
import Style from "./transitionSwitching.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 0.5s ease",
    transform: "translateX(-150px) ",
    backgroundColor:"red",
    opacity:"0",
  },
  entered: {
    transition: "all 0.5s ease",
    transform: "translateX(150px)",
    backgroundColor:"green",
    opacity:"1",
  },
  exiting: {
    transition: "all 0.5s ease",
    transform: "translateX(-150px)",
    backgroundColor: "blue",
    opacity:"1",
  },
  exited: {
   transition: "all 0.5s ease",
    transform: "translateX(150px)",
    backgroundColor: "gray",
    opacity:"0",
  },
};

const TransitionSwitch=()=> {
  const [name, setName] = useState(false);

  return (
    <div>
      <button onClick={() => setName(!name)}>Switching</button>
      <div className={Style.squareWrapper}>
      <SwitchTransition mode="in-out">

        <Transition
          key={name ? "aaa" : "bbb"}
          timeout={500}
          unmountOnExit
          mountOnEnter
          >

          {state => <div state={state} style={transitionStyle[state]} className={Style.square}>
  {name ? <p>AAA</p> : <p>BBB</p>}
          </div>}

        </Transition>

      </SwitchTransition>
      </div>

    </div>

  );
}

export default TransitionSwitch;
src\components\transitionSwitching\transitionSwitching.module.scss
/* 簡易リセット */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* スクロールバー常時表示 */
html {
  overflow-y: scroll;
}
.squareWrapper {
  margin-top: 100px;
  position: relative;
}
.square {
  background-color: blue;
  display: block;
  width: 200px;
  padding: 10px 20px;
  margin: 0 auto;
  color: #fff;
  text-align: center;
  position: absolute;
  left: calc(50% - 100px);
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";

import TransitionSwitching from "./components/transitionSwitching/TransitionSwiching";

function App() {
  return (
    <div className="App">
     <TransitionSwitching />
    </div>
  );
}

export default App;

mode

mode 内容
out-in 先に現在の要素がアウトし、完了後に新しい要素がインする
in-out 先に新しい要素がインし、完了後に現在の要素がアウトする

TransitionGroupとSwitchTransitionの使い分け

古い子要素のoutと新しい子要素のinを同時に行う場合は、TransitionGroupを使用します。

TransitionGroup

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

TransitionGroupでTransitionやCSSTransitionをラップします。
各TransitionやCSSTransitionにはinは不要で、代わりにユニークなKeyを設定します。

group.gif

上記サンプルを操作する場合はこちら

src\components\transitionList\TransitionList.js
import React, { useState,useRef } from "react";
import {TransitionGroup,Transition} from 'react-transition-group';
import Style from "./transitionList.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {

  entering: {
    transition: "all 0.2s ease",
    opacity:"0"
  },
  entered: {
    transition: "all 0.2s ease",
    opacity:"1"
  },
  exiting: {
    transition: "all 0.2s ease",
    opacity:"0"
  },
  exited: {
    transition: "all 0.2s ease",
    opacity:"0"
  },

};

//TransitionGroupコンポーネント
const TransitionList = () => {

  //inputに入力中の文字
  const [inputting,setInputting]=useState("");

  //input関連付け用のref
  const inputRef =useRef();

  //最後のID番号管理用
  const [lastId,setLastId]=useState(3);

  //初期アイテムリスト
  const initialList=[
    {id:0,word:"React"},
    {id:1,word:"Hooks"},
    {id:2,word:"Transition"},
  ];

  //アイテムリストに初期アイテムをセット
  const [items,setItems]=useState(initialList);

  //アイテムの追加処理
  const adder = () => {
    if(inputting){
      setItems(
      items=>[
        ...items,
       {id:lastId,
        word:inputting}
      ]
    )};

    //IDのインクリメント
    setLastId(prevId=>prevId+1);

    //inputのクリア
    setInputting("");
    inputRef.current.value="";
  };

  //リセット
  const reseter=()=>{
    setInputting("");
    inputRef.current.value="";
    setItems(initialList);
  }

  //form送信防止
  const stopSubmit=(e)=>{
    e.preventDefault();
  }

  return (

    <div className={Style.wrapper}>

      <form action=""
        onSubmit={stopSubmit}
        className={Style.controller}
      >

        <div className={Style.controllerAddGroup}>
          <input
            className={Style.controllerInput}
            ref={inputRef}
            type="text"
            onChange={
              (e)=>setInputting(e.target.value)
            }
          />

          <button
            className={Style.button}
            onClick={adder}
            disabled={!inputting ? true:false}
          >追加</button>
        </div>

        <button
          className={Style.button}
          onClick={reseter}>
          初期化
        </button>

      </form>

      <div className={Style.cardGroup}>

        <TransitionGroup className={Style.cardInner}>

          {items.map(({id,word})=>(

            <Transition key={id} timeout={200} >
              {(state) =>
                <div className={Style.cardShape} style={transitionStyle[state]} >
                  <button
                    className={Style.button}
                     onClick={() =>
                      setItems(items =>
                        items.filter(item => item.id !== id)
                      )
                    }
                  >
                  削除
                  </button>

                  <p className={Style.cardWord}> {word}</p>
                  <p className={Style.cardTransition}> {state}</p>
             </div>}

          </Transition>
              )
            )
          }
        </TransitionGroup>

      </div>

    </div>

  );

}

export default TransitionList;

src\components\transitionList\transitionList.module.scss
/* 簡易リセット */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* スクロールバー常時表示 */
html {
  overflow-y: scroll;
}

@media screen and (min-width: 651px) {
  .wrapper {
    margin: 0 auto;
    max-width: 600px;
  }

  .controller {
    background-color: #ffebee;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    margin-top: 40px;
    padding: 20px;
  }

  .controllerAddGroup {
    display: flex;
  }

  .controllerInput {
    border: none;
    padding: 10px;
    background-color: #ffcdd2;
    transition: all 0.5s ease;

    &:focus {
      background-color: #ffffff;
      outline: none;
    }
  }

  .button {
    appearance: none;
    background-color: #ec407a;
    border: none;
    color: #ffffff;
    cursor: pointer;
    font-family: inherit;
    font-size: 1rem;
    margin: 0;
    outline: none;
    padding: 10px 20px;
    transition: all 0.5s ease;
    width: 100px;

    &:hover {
      background-color: #ad1457;
    }

    &:disabled {
      opacity: 0;
      cursor: default;
    }
  }

  .cardGroup {
    display: flex;
    justify-content: space-between;
    margin-top: 40px;
    max-width: 1000px;
  }

  .cardInner {
    width: 100%;
  }

  .cardShape {
    background-color: #e3f2fd;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    padding: 20px;
    max-width: 600px;

    & + .cardShape {
      margin-top: 20px;
    }
  }

  .cardWord {
    color: #212121;
    display: inline;
    font-size: 2rem;
    font-weight: bold;
    line-height: 1.2;
    overflow: hidden;
    padding: 0 20px;
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    width: 400px;
  }

  .cardTransition {
    background-color: #1e88e5;
    color: #ffffff;
    border-radius: 4px;
    padding: 10px;
  }
}

@media screen and (max-width: 650px) {
  .wrapper {
    margin: 0 auto;
    max-width: 600px;
    padding: 0 10px;
  }

  .controller {
    background-color: #ffebee;
    border-radius: 4px;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    margin-top: 20px;
    padding: 15px;

    & > .button {
      margin-left: auto;
      margin-right: auto;
      display: block;
      margin-top: 10px;
    }
  }

  .controllerAddGroup {
    display: flex;
    width: 100%;
    justify-content: space-between;
  }

  .controllerInput {
    border: none;
    padding: 10px;
    background-color: #ffcdd2;
    transition: all 0.5s ease;
    width: 100%;

    &:focus {
      background-color: #ffffff;
      outline: none;
    }
  }

  .button {
    appearance: none;
    background-color: #ec407a;
    border: none;
    color: #ffffff;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.5rem;
    margin: 0;
    outline: none;
    padding: 10px;
    transition: all 0.5s ease;
    width: 100px;

    &:hover {
      background-color: #ad1457;
    }

    &:disabled {
      opacity: 0;
      cursor: default;
    }
  }

  .cardGroup {
    display: flex;
    justify-content: space-between;
    margin-top: 20px;
    max-width: 1000px;
  }

  .cardInner {
    width: 100%;
  }

  .cardShape {
    background-color: #e3f2fd;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    padding: 15px;
    max-width: 600px;

    & + .cardShape {
      margin-top: 10px;
    }
  }

  .cardWord {
    color: #212121;
    display: inline;
    font-size: 1rem;
    font-weight: bold;
    line-height: 1.2;
    overflow: hidden;
    padding: 0 20px;
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    width: 400px;
  }

  .cardTransition {
    background-color: #1e88e5;
    color: #ffffff;
    border-radius: 4px;
    padding: 5px;
  }
}

src\App.js
import React from "react";

import TransitionList from "./components/transitionList/TransitionList";

function App() {
  return (
    <div className="App">
     <TransitionList />
    </div>
  );
}

export default App;

リポジトリ

https://github.com/takeshisakuma/rtg

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away