LoginSignup
157
85

More than 3 years have passed since last update.

ワイ「アニメーションするにはこのuseTransitionってのを使えばええんか?」

Posted at

社長「やめ太郎くん

ワイ「なんでっか社長、ワイは今Reactのアプリを半分だけVueに書き換える作業で忙しいんでっせ」

ハスケル子「(何でそんな意味不明なことを……)」

社長「せやったな、これからはVueの時代やからVueの使用実績を増やさなあかんねん」

ワイ「とはいえReactも今年公式ドキュメントの日本語版が出たり勢いづいとるから捨てがたい」

社長「せやから半々にしてどっちも取り入れるんや! 素晴らしい施策やろ!」

ワイ「さすが社長!」

ハリー先輩「(案件を半々にするんちゃうのかい!)」

ハスケル子「(私は何でこんな所でインターンしているんだろう)」

※ この記事は全面無職やめ太郎さんリスペクトワイ記法でお送りする二次創作記事です。(6ヶ月ぶり3回目

Reactでアニメーションを実装したい

社長「さて、今回はアプリにいい感じのアニメーションを追加してもらいたいんや。これからはUXの時代やからな」

社長「React側に実装を頼むで」

ワイ「承知しましたで」

ハスケル子「やめ太郎さんはReactでアニメーションの実装したことあるんですか?」

ワイ「無いで※この記事における設定です。現実とは異なる可能性がありますのでご注意ください。

ワイ「でもまあCSSでtransitionってのを使えばええことくらいは知っとるで」

ワイ「アニメーションの実装くらい楽勝やな!」

〜数時間後〜

ワイ「あかん、こんがらがってきたで」

ワイ「今回はコンポーネントが増えたり減ったりするんや」

娘(4歳)「うん」

ワイ「新しいコンポーネントが増えたときにアニメーションさせなあかん」

娘「うん」

ワイ「でもアニメーションさせるにはまずアニメーション前の状態をレンダリングせなあかん」

娘「うん」

ハスケル子「(何でこの人は娘さんに相談してるんだろう)」

娘「transitionによるアニメーションはあくまでCSSの変化をアニメーションしてくれるものだから、変化前のCSSの状態を一旦作ることが必要になるんだよね」

ワイ「せや」

娘「コンポーネントが出現するとき、アニメーションの前の状態をまず描画→その後アニメーションさせるという2段階の処理が必要なんだね」

ワイ「せや」

娘「これを実装するには、コンポーネントがマウントされていない状態→コンポーネントがマウントされた状態(CSSは変化前の状態)→コンポーネントがマウントされた状態(CSSは変化後の状態)という2段階の状態変化を実装する必要があるんだね」

ワイ「せや」

ハスケル子「(この記事娘さんだけでいいんじゃないかな)」

ワイ「コンポーネントが消えるときも難しいんや」

娘「コンポーネントを消したいときはアニメーションが終わるまでコンポーネントを残しておいて、それから消す必要があるんだね」

ワイ「せや」

娘「意味上はもう消えているコンポーネントを、アニメーションというUIの都合で残しておかないといけないのがReact的じゃなくて辛いよね」

ハスケル子「そういうロジックを組めばいいじゃないですか」

ワイ「まあできないわけやないで」

ワイ「でも何かコードが汚くなってまうんや」

useTransitionを知る

ハスケル子「ふふふ」

ワイ「何や気味の悪い」

ハスケル子「その辺をいい感じにやってくれるのがuseTransitionですよ」

ワイ「ほう、ちょっと調べてみよか……おお、useTransitionのドキュメントがあったで」

ハスケル子「そうです、useTransitionのドキュメントに書いてある通りこれで簡単にアニメーションが実装できます」

ワイ「でもこれ次の画面に移るときに使うとか書いてあるで」

useTransition allows components to avoid undesirable loading states by waiting for content to load before transitioning to the next screen. It also allows components to defer slower, data fetching updates until subsequent renders so that more crucial updates can be rendered immediately.

useTransitionのドキュメントから引用)

ハスケル子「ああ、useTransitionはルーターと組み合わせてページ遷移のアニメーションにも使えるんですよ」

4b234568bc38eb568c5fa5be7a6f29da.png

useTransitionのドキュメントから引用)

ハスケル子「というかやめ太郎さん英語のドキュメント読めるんですか」

ワイ「当たり前や」

ワイ「人様のキャラやからできるだけ持ち上げとこ思ってな

ハスケル子「……」

ハスケル子「そのネタ好きですね」

ハスケル子「あと記事冒頭で社長をアホキャラにしてますけど実在の会社とは関係ありませんからね」

ワイ「とにかく」

ワイ「このuseTransitionを使ってアニメーションを実装してみるで」

ハスケル子「まあ使い方がさっきのドキュメントに書いてあるのでそんなに難しくないですよ」

ワイ「探したらuseTransitionの日本語解説記事もあったから余裕やな!(宣伝)」

useTansitionでアニメーションを実装してみた

〜翌日〜

ワイ「半分だけ実装できたで

ハスケル子「半分……?」

ワイ「コンポーネントが消えるときはうまく行くんやけど出てくるときがまだ実装できてへんねん」

ハスケル子「useTransitionをどんな使い方したら消えるときだけなんて挙動になるのか逆に気になりますよ」

ワイ「まあ見てくれや」

ハスケル子「……ボタンがあって押すと青い箱が出たり消えたりするんですね」

サンプルのスクリーンショット

ワイ「消えるときだけアニメーションがかかるやろ」

ハスケル子「どれどれ実装は……!?

ワイ「なんや珍しく驚いた顔なんかしよって」

ワイ「あとワイは本家と違うて絵心は無いから驚いた顔の絵とかは無いで」

index.js
  const [startTransition, isPending] = useTransition({
    timeoutMs: 1000
  });

ハスケル子「なんですかこのuseTransitionって」

ワイ「なんや、ハスケル子ちゃんが教えてくれたんやろ」

ハスケル子「私が言ってたのは誰もが知ってるuseTransitionのことですよ」

ハスケル子「useTransitionなんて意味不明な機能知りませんよ、というかまだ実験段階じゃないですか」

ワイ「なんやuseTransitionやなくてuseTransitionのこと言っとったんかい! 紛らわしいわ!」

ワイ「ワイもよう分からんようなってきたで、こういうときのための娘ちゃんや」

娘「パパが言ってるのはReact Concurrent ModeuseTransitionで、ハスケル子ちゃんが言っているのはreact-springuseTransitionだよ!」

娘「この記事で扱うのはReact Concurrent ModeのuseTransitionで、react-springは特に関係ないよ!」

ワイ「よう分かったで、さすがワイの娘や!」

ハスケル子「(何なんだろうこの茶番)」

娘「Concurrent Modeの詳しい説明とかuseTransitionの解説は筆者の既存記事を見てね!(宣伝)」

ハスケル子「それで、このサンプルはどうやってアニメーションしてるんですか?」

ワイ「せやったな、まず箱が出とるかどうかはshowというステートで管理するんや」

index.js
  const [show, setShow] = useState(false);

ワイ「レンダリング部分ではshowtrueのときだけBoxをレンダリングするんや」

index.js
  return (
    <div className="App">
      <h1>Animation Sample</h1>
      <p>
        <button onClick={toggle}>toggle</button>
      </p>
      {show ? <Box show={show && !isPending} /> : null}
      <Suspense fallback={null}>
        <Waiter timer={timer} />
      </Suspense>
    </div>
  );

ワイ「Boxshow={false}のときはopacity: 0show={true}のときはopacity: 1や」

ハスケル子「ここにあるisPendingがよく分からないですけど、ここに秘密があるんですね」

ワイ「せや」

ワイ「toggle関数ではshowtruefalseを切り替えるんやけど」

ワイ「何も考えんでやるとshowfalseになった瞬間に箱がDOMから消えるからアニメーションにならんのや」

娘「そこでuseTransitionの出番だね」

ワイ「せや」

ハスケル子「よく見るとuseTransitionを呼んでstartTransitionisPendingを取得していますね」

ワイ「せや、そしてstartTransitiontoggleの中で使われとるで」

index.js
  const toggle = () => {
    if (show) {
      startTransition(() => {
        setShow(false);
        setTimer(new Timer(500));
      });
    } else {
      setShow(true);
    }
  };

ワイ「箱が出とるときにtoggleを呼ぶとstartTransitionが使われるで」

ハスケル子「startTransitionはコールバック関数を受け取るんですね」

ワイ「せや、これはすぐ呼ばれるで」

ワイ「この中でステートの更新をするとuseTransitionの効果が発揮されるんや」

ハスケル子「今回はsetShow(false)setTimer(new Timer(500))ですね」

娘「Timerってなに?」

ワイ「ええ質問やな、でもそれは後で説明するで」

ワイ「useTransitionの効果はな、ステート更新によって発生したレンダリングが完了するまで画面の更新が遅延されるっちゅうことや」

ハスケル子「???」

ワイ「ステートを更新したら当然再レンダリングが起こるやろ」

ハスケル子「はい」

ワイ「実はこのレンダリングが完了するまで500ミリ秒かかるんや」

娘「Timerのおかげだね!」

ワイ「せや、ワイが説明する前に理解するとはさすが娘ちゃんやで」

ハスケル子「(あとで娘ちゃんに説明してもらおう)」

ワイ「レンダリング完了まで500ミリ秒かかるってことはsetShow(false)が画面に反映されるまで500ミリ秒かかるってことやで」

ハスケル子「なるほど、その間にアニメーションさせればいいんですね」

ワイ「物分かりがええやないか」

ハスケル子「でも、肝心のアニメーションはどうやっているんですか?」

ワイ「ここがisPendingの出番や」

index.js
      {show ? <Box show={show && !isPending} /> : null}

ワイ「isPending画面の更新が遅延されている最中にtrueになるんや。普段はfalseやで」

娘「図を用意したからこれを見ながら説明するよ」

ワイ「さすが娘ちゃんや、天才やな!」

スクリーンショット 2019-12-08 22.52.06.png

ワイ「最初はshowtrueisPendingfalseやから<Box show={true} />がレンダリングされとるで」

ワイ「ボタンを押すとtoggle()が呼ばれるんや」

ハスケル子「図によると、toggleを呼んだ瞬間、つまりstartTransitionの中でsetShow(false)を呼んだ瞬間はshowtrueのままでisPendingtrueになる」

ハスケル子「そうなると<Box show={false} />がレンダリングされるんですね」

ワイ「せや」

ワイ「BoxはCSSでtransition: opacity 500ms linearと指定してあるさかい、ここでアニメーションが起こるで」

ハスケル子「そして500ミリ秒後にshowfalseになる」

ワイ「せや、これはstartTransitionによる遅延が終わったからやな」

ワイ「isPendingfalseに戻るんや」

ハスケル子「500ミリ秒というのはちょうどアニメーションにかかる時間ですね」

ワイ「せや、アニメーションが終わった頃にuseTransitionの遅延が終わってBoxが消されるっちゅう寸法や」

ハスケル子「なるほど……なんとなく分かったような気がします」

ワイ「コードのURLをもう一回貼っとくから自分でいろいろ試してみるんやで」

レンダリング遅延の仕組み

ハスケル子「ところで、レンダリングが500ミリ秒遅延されたのはどうしてですか?」

ハスケル子「明らかにこのTimerが怪しいですよね」

index.js
  const [timer, setTimer] = useState(null);
  // ... 中略 ...
        setTimer(new Timer(500));

ハスケル子「timerはここで描画に使われています」

index.js
      <Suspense fallback={null}>
        <Waiter timer={timer} />
      </Suspense>

娘「じゃあまずTimerについて説明するよ!」

娘「実装は別ファイルに書いてあるよ」

Timer.js
export class Timer {
  constructor(duration) {
    const timer = new Promise(resolve => setTimeout(resolve, duration));
    this.done = false;
    this.promise = timer.then(() => {
      this.done = true;
    });
  }
  throwIfNotDone() {
    if (!this.done) {
      throw this.promise;
    }
  }
}

娘「new Timer(duration)が実行されると、durationミリ秒後に解決されるPromiseが作られるよ」

ハスケル子「それはthis.promiseに入っているんですね」

娘「そして、このPromiseが解決されたらthis.donetrueになるよ」

ハスケル子「このthrowIfNotDoneメソッドは何ですか?」

娘「これはまだPromiseが解決されていなかったらPromiseをthrowするというメソッドだよ」

ワイ「throwってエラーが出たときに使う奴ちゃうんか?」

ハスケル子「言語仕様上はthrowは何でも投げられるんですよね」

娘「このPromiseを投げるというのがReactのConcurrent Modeの特徴なんだよ」

娘「Reactは、コンポーネントがPromiseを投げたらそのコンポーネントの描画が先延ばしにされたと判断するよ」

ハスケル子「ということは、先延ばしの実態はここにあったんですね」

ワイ「せや、ここで投げられたPromiseをuseTransitionが検知して画面更新の遅延を発生させるんや」

娘「まとめると、new Timer(500)は解決に500ミリ秒かかるPromiseを用意したってことなんだよ!」

ハスケル子「じゃあ、これを使う側はどうなっているんですか?」

index.js
      <Suspense fallback={null}>
        <Waiter timer={timer} />
      </Suspense>
index.js
function Waiter({ timer }) {
  if (timer) timer.throwIfNotDone();
  return null;
}

ワイ「WaiterコンポーネントがPromiseをthrowする役目を持っとるで」

ワイ「渡されたtimerがまだ解決していなかったらPromiseを投げるんや」

娘「つまりWaiterは渡されたtimerがまだ完了していなかったら完了するまでコンポーネントの描画を先延ばしにするという役目のコンポーネントだよ」

ハスケル子「Waiterの周りにあるSuspenseは何ですか?」

ワイ「Promiseをthrowするコンポーネントは周りをSuspenseで囲む必要があるんや」

ワイ「細かいことは筆者の既存記事を読むんやで」

娘「まとめるとこうだよ」

娘「startTransitionの中でsetShow(false)と同時にsetTimer(new Timer(500))を実行したよね」

娘「これにより再描画が発生するけどWaiterがPromiseを投げることで描画が先延ばしにされる」

ハスケル子「new Timer(500)なので500ミリ秒先延ばしになるんですね」

娘「あとはさっきパパが説明した通りだよ」

ワイ「せや」

ワイ「先延ばしになっている間にuseTransitionの効果でisPendingtrueになるんや」

ワイ「かわいい娘ちゃんが作った図をもう一度出しておくで」

スクリーンショット 2019-12-08 22.52.06.png

ワイ「以上がワイの実装の説明やで、随分長くなってもうたな」

ハスケル子「でもまだ半分しか実装できてないんですよねこれ

ワイ「うっ……」

ワイ「待っとけや、もう半分もすぐに実装してくるで」

娘「がんばれパパ!」

出てくるときのアニメーションの実装

〜1ヶ月後〜

ハスケル子「いやどれだけ時間かかってるんですか」

ワイ「仕方ないやろ、筆者が思いつくまで1ヶ月くらいかかったんや(実話)」

ワイ「まあ上んとこまで記事書いてから考えなおしたら一瞬でできたんやけどな」

娘「考えをまとめ直すのは大事だね!」

ワイ「完全版の実装はこれやで」

ハスケル子「どれどれ……あまり変わってませんね」

ワイ「変わったのは1箇所だけや、toggleの定義部分な」

index.js
  const toggle = () => {
    if (show) {
      startTransition(() => {
        setShow(false);
        setTimer(new Timer(500));
      });
    } else {
      setShow(true);
      startTransition(() => {
        setTimer(new Timer(10));
      });
    }
  };

ハスケル子「falseからtrueになるときは、setShow(true)startTransitionの外で実行してsetTimerだけ中で実行するんですね」

ワイ「せや」

娘「こうすると、toggleを押した直後はshowtrueisPendingtrueの状態になるよ」

ハスケル子「そのときは……<Box show={false} />が描画されますね」

娘「そして、10ミリ秒後にisPendingfalseになるよ」

ワイ「図にするとこうやで」

スクリーンショット 2019-12-08 23.03.06.png

ハスケル子「今回も、何も無い状態からまずopacity: 0の状態を描画してそれからopacity: 1にしているんですね」

ハスケル子「10ミリ秒というのは何の意味があるんですか?」

ワイ「今回はopacity: 0の状態を描画するのは一瞬だけでええんや」

ワイ「すぐopacity: 1にしないとアニメーションが開始せんからな」

ワイ「一瞬だけopacity: 0を描画したかったからなるべく短い時間にしたんやけど」

ワイ「0とかにすると短すぎてうまくいかへんのや」

娘「0ミリ秒後というのは短すぎてReactが遅延したと認識しないみたいだね」

ハスケル子「なるほど……あれ」

娘「どうしたの?」

ハスケル子「何回もボタンを押しているとたまに箱が出るときのアニメーションが無いですね」

ワイ「せや、10ミリ秒でもたまに短すぎてuseTransitionが働かないときがあるで」

ワイ「Reactは気まぐれやからな」

ハスケル子「100ミリ秒とかもっと長い時間にするのは駄目なんですか?」

ワイ「そしたらアニメーションが始まるのが100ミリ秒間遅れてまうやろ」

娘「逆に言えば、アニメーション開始を遅らせたいときはそれも有効な対策だよ!」

今回の実装の欠点

ハスケル子「ところで」

ハスケル子「ボタンを連打したときの動きが変ですよね?」

ワイ「うっ」

ハスケル子「特に、箱が消えている途中でボタン押しても何も起きずにそのまま消えますよね」

ハスケル子「消えている途中でボタンを押したらshowfalseからtrueに戻って箱が復活するべきじゃないですか?」

ハスケル子「宣言的UIが聞いてあきれますね

ワイ「そうでもないで」

ワイ「まあワイの実装があかんのは確かやな、今後の課題や」

娘「でも今回showtrueからfalseに変えるのはstartTransitionの中だよね」

娘「ということはshowfalseに変えるのに500ミリ秒かかっているってことなんだよ」

ワイ「せや」

ワイ「まだshowtrueのままなんやから、何回ボタン押してもfalseになるだけのほうがええんちゃうか?」

娘「図にするとこうだね」

娘「toggleはあくまでshowtruefalseかで動作を変えているよ」

スクリーンショット 2019-12-08 23.10.29.png

ハスケル子「そ……それは確かに」

ワイ「せや! 次のインターンの課題は今のバグを修正することにするで!」

ハスケル子「うっ……藪蛇でした」

まとめ

ハスケル子「結局この記事って何がテーマだったんですか?」

ワイ「おう、テーマな」

ワイ「useTransitionアニメーションをタイトルに入れてreact-spring勢を釣りたいがテーマや」

ハスケル子「ぐっ……まんまと釣りに加担してしまったわけですか」

社長「おい」

ワイ「しゃ、社長!」

社長「お前この記事が今年イチ!お勧めしたいテクニック by ゆめみ feat.やめ太郎 Advent Calendar 2019の9日目って知っとんのか? テーマは「今年イチ!お勧めしたいテクニック」だったやろがい!」

ワイ「も、もちろん分かっておりまっせ! 今年イチお勧めしたいテクニックは何と言ってもuseTransitionですわ!」

娘「react-springじゃなくてReact Concurrent ModeのuseTransitionだよ!」

ワイ「筆者の前記事でも言っとった通り、useTransitionは状態を複数同時に管理できるんですわ」

ハスケル子「具体的には、『ステート変更反映前でisPendingtrueの状態』と『ステート変更反映後の状態』ですね」

ワイ「その2つの状態をうまく扱うことでアニメーションのような時間差処理が必要な処理を綺麗に扱うことができるんでっせ!」

社長「(綺麗だったか……?)」

ワイ「一定時間レンダリングを遅延させるためにTimerWaiterを使うのはなかなか有用なテクニックとちゃいますか!?」

ハスケル子「(Waiterとかかなり無理やりでは……?)」

ハリー先輩「(絶対そんな使い方想定されてへんやろ)」

ワイ「他にもuseTransitionは色々な使い道があるで! ぜひ使いこなしてや!」

ワイ「まだ正式リリースされとらんけどな!

娘「今年中にリリースされる可能性はかなり低いよ!

ハスケル子「(今年イチとは何だったのか)」

余談:もうひとつのお勧めテクニック

ワイ「もうひとつ考えたことがあるんや」

ハスケル子「何ですか?」

ワイ「実は真にお勧めしたいテクニック、それはワイ記法や」

ハスケル子「……」

ワイ「筆者はこう見えてもこの記事でワイ記法は3本目や、全部今年やで」

ハスケル子「何回も人のネタパクって何やってるんですか一体……」

ハスケル子「それどころかこういう第四の壁越えた茶番は客観的に見ると結構痛いですよ」

ワイ「しゃあないやろ、ワイはワイ記法を使うときは最初から最後まで貫く主義なんや」

ワイ「『くぅ〜疲れましたw』よりはマシやろ」

ハスケル子「(人のネタで相撲取っておいて何を言ってるんだか)」

ワイ「まあやってみて考えたことがあるんや、第三者の目線ってやつやな」

ハスケル子「はあ」

ワイ「普段ワイは文章構成とか気を使って書くほうなんやけど」

ワイ「会話形式やと文章構成もクソも無いんや」

娘「思考をそのまま書き下すのが主流みたいだね」

ワイ「でも本家の記事は分かりやすい言うてめっちゃ評判ええやろ」

ワイ「ワイなりにその理由を考えたんや」

ハスケル子「理由……」

ワイ「まあ一言で言うとキャラ付けや」

ワイ「キャラごとの役割を明確にするのが分かりやすさの理由の一つやと思ってこの記事でも実践してみたで」

ハスケル子「私は言うまでもなく聞き手役ですね」

ワイ「せや、解説役はワイと娘ちゃんや」

ハスケル子「2人はどう違うんですか?」

ワイ「まあはっきりした違いがあるわけではないんやけどな」

ワイ「ワイはどちらかというと読者寄り、娘ちゃんは教師役って立ち位置にしてみたで」

ワイ「基本はワイが実装者の目線で説明して娘ちゃんがライブラリの動きとかを解説するんや」

ハスケル子「3人の場合は聞き手、読者役、教師役と分けるといいんですね」

ワイ「まあ一例や」

ワイ「あと解説役が2人いるとやりやすいこともあるで」

娘「特に複数の話題がある場合や解説を後回しにする場合は、話題の担当者を明確にすることで読者の記憶を助ける効果もあると思うよ!」

ワイ「ほら、娘ちゃんは教師役さかい賢そうなしゃべり方なんや」

ハスケル子「賢そうというかなんか地の文に近くて堅いですね」

ワイ「まあ感覚で書いとる部分もあるんやけど、こういう所もキャラ付けに気を遣ったんやで」

ワイ「あとワイはレンダリングのことを『レンダリング』と呼ぶけど娘ちゃんは『描画』って呼んどるで」

ハスケル子「はあ」

ワイ「まあワイ記法も奥が深いってことを言いたかったんや」

ワイ「結構楽しいからみんなもワイ記法で記事書いてみてや!」

〜完〜

157
85
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
157
85