社長「やめ太郎くん」
ワイ「なんでっか社長、ワイは今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はルーターと組み合わせてページ遷移のアニメーションにも使えるんですよ」
(useTransitionのドキュメントから引用)
ハスケル子「というかやめ太郎さん英語のドキュメント読めるんですか」
ワイ「当たり前や」
ハスケル子「……」
ハスケル子「そのネタ好きですね」
ハスケル子「あと記事冒頭で社長をアホキャラにしてますけど実在の会社とは関係ありませんからね」
ワイ「とにかく」
ワイ「このuseTransition
を使ってアニメーションを実装してみるで」
ハスケル子「まあ使い方がさっきのドキュメントに書いてあるのでそんなに難しくないですよ」
ワイ「探したら**useTransitionの日本語解説記事**もあったから余裕やな!(宣伝)」
useTansitionでアニメーションを実装してみた
〜翌日〜
ワイ「半分だけ実装できたで」
ハスケル子「半分……?」
ワイ「コンポーネントが消えるときはうまく行くんやけど出てくるときがまだ実装できてへんねん」
ハスケル子「useTransition
をどんな使い方したら消えるときだけなんて挙動になるのか逆に気になりますよ」
ワイ「まあ見てくれや」
ハスケル子「……ボタンがあって押すと青い箱が出たり消えたりするんですね」
ワイ「消えるときだけアニメーションがかかるやろ」
ハスケル子「どれどれ実装は……!?」
ワイ「なんや珍しく驚いた顔なんかしよって」
ワイ「あとワイは本家と違うて絵心は無いから驚いた顔の絵とかは無いで」
const [startTransition, isPending] = useTransition({
timeoutMs: 1000
});
ハスケル子「なんですかこの**useTransition**って」
ワイ「なんや、ハスケル子ちゃんが教えてくれたんやろ」
ハスケル子「私が言ってたのは誰もが知ってる**useTransition**のことですよ」
ハスケル子「**useTransitionなんて意味不明な機能知りませんよ、というかまだ実験段階**じゃないですか」
ワイ「なんや**useTransitionやなくてuseTransition**のこと言っとったんかい! 紛らわしいわ!」
ワイ「ワイもよう分からんようなってきたで、こういうときのための娘ちゃんや」
娘「パパが言ってるのはReact Concurrent Modeの**useTransitionで、ハスケル子ちゃんが言っているのはreact-springのuseTransition**だよ!」
娘「この記事で扱うのはReact Concurrent ModeのuseTransitionで、react-springは特に関係ないよ!」
ワイ「よう分かったで、さすがワイの娘や!」
ハスケル子「(何なんだろうこの茶番)」
娘「Concurrent Modeの詳しい説明とかuseTransitionの解説は筆者の既存記事を見てね!(宣伝)」
ハスケル子「それで、このサンプルはどうやってアニメーションしてるんですか?」
ワイ「せやったな、まず箱が出とるかどうかはshow
というステートで管理するんや」
const [show, setShow] = useState(false);
ワイ「レンダリング部分ではshow
がtrue
のときだけBox
をレンダリングするんや」
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>
);
ワイ「Box
はshow={false}
のときはopacity: 0
でshow={true}
のときはopacity: 1
や」
ハスケル子「ここにあるisPending
がよく分からないですけど、ここに秘密があるんですね」
ワイ「せや」
ワイ「toggle
関数ではshow
のtrue
とfalse
を切り替えるんやけど」
ワイ「何も考えんでやるとshow
がfalse
になった瞬間に箱がDOMから消えるからアニメーションにならんのや」
娘「そこでuseTransition
の出番だね」
ワイ「せや」
ハスケル子「よく見るとuseTransition
を呼んでstartTransition
とisPending
を取得していますね」
ワイ「せや、そしてstartTransition
はtoggle
の中で使われとるで」
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
の出番や」
{show ? <Box show={show && !isPending} /> : null}
ワイ「isPending
は画面の更新が遅延されている最中にtrue
になるんや。普段はfalse
やで」
娘「図を用意したからこれを見ながら説明するよ」
ワイ「さすが娘ちゃんや、天才やな!」
ワイ「最初はshow
がtrue
でisPending
はfalse
やから<Box show={true} />
がレンダリングされとるで」
ワイ「ボタンを押すとtoggle()
が呼ばれるんや」
ハスケル子「図によると、toggle
を呼んだ瞬間、つまりstartTransition
の中でsetShow(false)
を呼んだ瞬間はshow
はtrue
のままでisPending
もtrue
になる」
ハスケル子「そうなると<Box show={false} />
がレンダリングされるんですね」
ワイ「せや」
ワイ「Box
はCSSでtransition: opacity 500ms linear
と指定してあるさかい、ここでアニメーションが起こるで」
ハスケル子「そして500ミリ秒後にshow
がfalse
になる」
ワイ「せや、これはstartTransition
による遅延が終わったからやな」
ワイ「isPending
もfalse
に戻るんや」
ハスケル子「500ミリ秒というのはちょうどアニメーションにかかる時間ですね」
ワイ「せや、アニメーションが終わった頃にuseTransition
の遅延が終わってBox
が消されるっちゅう寸法や」
ハスケル子「なるほど……なんとなく分かったような気がします」
ワイ「コードのURLをもう一回貼っとくから自分でいろいろ試してみるんやで」
レンダリング遅延の仕組み
ハスケル子「ところで、レンダリングが500ミリ秒遅延されたのはどうしてですか?」
ハスケル子「明らかにこのTimer
が怪しいですよね」
const [timer, setTimer] = useState(null);
// ... 中略 ...
setTimer(new Timer(500));
ハスケル子「timer
はここで描画に使われています」
<Suspense fallback={null}>
<Waiter timer={timer} />
</Suspense>
娘「じゃあまずTimer
について説明するよ!」
娘「実装は別ファイルに書いてあるよ」
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.done
がtrue
になるよ」
ハスケル子「このthrowIfNotDone
メソッドは何ですか?」
娘「これはまだPromiseが解決されていなかったらPromiseをthrowするというメソッドだよ」
ワイ「throw
ってエラーが出たときに使う奴ちゃうんか?」
ハスケル子「言語仕様上はthrow
は何でも投げられるんですよね」
娘「このPromise
を投げるというのがReactのConcurrent Modeの特徴なんだよ」
娘「Reactは、コンポーネントがPromise
を投げたらそのコンポーネントの描画が先延ばしにされたと判断するよ」
ハスケル子「ということは、先延ばしの実態はここにあったんですね」
ワイ「せや、ここで投げられたPromiseをuseTransition
が検知して画面更新の遅延を発生させるんや」
娘「まとめると、new Timer(500)
は解決に500ミリ秒かかるPromiseを用意したってことなんだよ!」
ハスケル子「じゃあ、これを使う側はどうなっているんですか?」
<Suspense fallback={null}>
<Waiter timer={timer} />
</Suspense>
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
の効果でisPending
がtrue
になるんや」
ワイ「かわいい娘ちゃんが作った図をもう一度出しておくで」
ワイ「以上がワイの実装の説明やで、随分長くなってもうたな」
ハスケル子「でもまだ半分しか実装できてないんですよねこれ」
ワイ「うっ……」
ワイ「待っとけや、もう半分もすぐに実装してくるで」
娘「がんばれパパ!」
出てくるときのアニメーションの実装
〜1ヶ月後〜
ハスケル子「いやどれだけ時間かかってるんですか」
ワイ「仕方ないやろ、筆者が思いつくまで1ヶ月くらいかかったんや(実話)」
ワイ「まあ上んとこまで記事書いてから考えなおしたら一瞬でできたんやけどな」
娘「考えをまとめ直すのは大事だね!」
ワイ「完全版の実装はこれやで」
ハスケル子「どれどれ……あまり変わってませんね」
ワイ「変わったのは1箇所だけや、toggle
の定義部分な」
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
を押した直後はshow
がtrue
でisPending
もtrue
の状態になるよ」
ハスケル子「そのときは……<Box show={false} />
が描画されますね」
娘「そして、10ミリ秒後にisPending
がfalse
になるよ」
ワイ「図にするとこうやで」
ハスケル子「今回も、何も無い状態からまずopacity: 0
の状態を描画してそれからopacity: 1
にしているんですね」
ハスケル子「10ミリ秒というのは何の意味があるんですか?」
ワイ「今回はopacity: 0
の状態を描画するのは一瞬だけでええんや」
ワイ「すぐopacity: 1
にしないとアニメーションが開始せんからな」
ワイ「一瞬だけopacity: 0
を描画したかったからなるべく短い時間にしたんやけど」
ワイ「0
とかにすると短すぎてうまくいかへんのや」
娘「0ミリ秒後というのは短すぎてReactが遅延したと認識しないみたいだね」
ハスケル子「なるほど……あれ」
娘「どうしたの?」
ハスケル子「何回もボタンを押しているとたまに箱が出るときのアニメーションが無いですね」
ワイ「せや、10ミリ秒でもたまに短すぎてuseTransition
が働かないときがあるで」
ワイ「Reactは気まぐれやからな」
ハスケル子「100ミリ秒とかもっと長い時間にするのは駄目なんですか?」
ワイ「そしたらアニメーションが始まるのが100ミリ秒間遅れてまうやろ」
娘「逆に言えば、アニメーション開始を遅らせたいときはそれも有効な対策だよ!」
今回の実装の欠点
ハスケル子「ところで」
ハスケル子「ボタンを連打したときの動きが変ですよね?」
ワイ「うっ」
ハスケル子「特に、箱が消えている途中でボタン押しても何も起きずにそのまま消えますよね」
ハスケル子「消えている途中でボタンを押したらshow
がfalse
からtrue
に戻って箱が復活するべきじゃないですか?」
ハスケル子「宣言的UIが聞いてあきれますね」
ワイ「そうでもないで」
ワイ「まあワイの実装があかんのは確かやな、今後の課題や」
娘「でも今回show
をtrue
からfalse
に変えるのはstartTransition
の中だよね」
娘「ということは**show
をfalse
に変えるのに500ミリ秒かかっている**ってことなんだよ」
ワイ「せや」
ワイ「まだshow
がtrue
のままなんやから、何回ボタン押してもfalse
になるだけのほうがええんちゃうか?」
娘「図にするとこうだね」
娘「toggle
はあくまでshow
がtrue
かfalse
かで動作を変えているよ」
ハスケル子「そ……それは確かに」
ワイ「せや! 次のインターンの課題は今のバグを修正することにするで!」
ハスケル子「うっ……藪蛇でした」
まとめ
ハスケル子「結局この記事って何がテーマだったんですか?」
ワイ「おう、テーマな」
ワイ「useTransitionとアニメーションをタイトルに入れてreact-spring勢を釣りたいがテーマや」
ハスケル子「ぐっ……まんまと釣りに加担してしまったわけですか」
社長「おい」
ワイ「しゃ、社長!」
社長「お前この記事が今年イチ!お勧めしたいテクニック by ゆめみ feat.やめ太郎 Advent Calendar 2019の9日目って知っとんのか? テーマは**「今年イチ!お勧めしたいテクニック」**だったやろがい!」
ワイ「も、もちろん分かっておりまっせ! 今年イチお勧めしたいテクニックは何と言ってもuseTransition
ですわ!」
娘「react-springじゃなくてReact Concurrent ModeのuseTransition
だよ!」
ワイ「筆者の前記事でも言っとった通り、useTransition
は状態を複数同時に管理できるんですわ」
ハスケル子「具体的には、『ステート変更反映前でisPending
がtrue
の状態』と『ステート変更反映後の状態』ですね」
ワイ「その2つの状態をうまく扱うことでアニメーションのような時間差処理が必要な処理を綺麗に扱うことができるんでっせ!」
社長「(綺麗だったか……?)」
ワイ「一定時間レンダリングを遅延させるためにTimer
とWaiter
を使うのはなかなか有用なテクニックとちゃいますか!?」
ハスケル子「(Waiter
とかかなり無理やりでは……?)」
ハリー先輩「(絶対そんな使い方想定されてへんやろ)」
ワイ「他にもuseTransition
は色々な使い道があるで! ぜひ使いこなしてや!」
ワイ「まだ正式リリースされとらんけどな!」
娘「今年中にリリースされる可能性はかなり低いよ!」
ハスケル子「(今年イチとは何だったのか)」
余談:もうひとつのお勧めテクニック
ワイ「もうひとつ考えたことがあるんや」
ハスケル子「何ですか?」
ワイ「実は真にお勧めしたいテクニック、それはワイ記法や」
ハスケル子「……」
ワイ「筆者はこう見えてもこの記事でワイ記法は3本目や、全部今年やで」
ハスケル子「何回も人のネタパクって何やってるんですか一体……」
ハスケル子「それどころかこういう第四の壁越えた茶番は客観的に見ると結構痛いですよ」
ワイ「しゃあないやろ、ワイはワイ記法を使うときは最初から最後まで貫く主義なんや」
ワイ「『くぅ〜疲れましたw』よりはマシやろ」
ハスケル子「(人のネタで相撲取っておいて何を言ってるんだか)」
ワイ「まあやってみて考えたことがあるんや、第三者の目線ってやつやな」
ハスケル子「はあ」
ワイ「普段ワイは文章構成とか気を使って書くほうなんやけど」
ワイ「会話形式やと文章構成もクソも無いんや」
娘「思考をそのまま書き下すのが主流みたいだね」
ワイ「でも本家の記事は分かりやすい言うてめっちゃ評判ええやろ」
ワイ「ワイなりにその理由を考えたんや」
ハスケル子「理由……」
ワイ「まあ一言で言うとキャラ付けや」
ワイ「キャラごとの役割を明確にするのが分かりやすさの理由の一つやと思ってこの記事でも実践してみたで」
ハスケル子「私は言うまでもなく聞き手役ですね」
ワイ「せや、解説役はワイと娘ちゃんや」
ハスケル子「2人はどう違うんですか?」
ワイ「まあはっきりした違いがあるわけではないんやけどな」
ワイ「ワイはどちらかというと読者寄り、娘ちゃんは教師役って立ち位置にしてみたで」
ワイ「基本はワイが実装者の目線で説明して娘ちゃんがライブラリの動きとかを解説するんや」
ハスケル子「3人の場合は聞き手、読者役、教師役と分けるといいんですね」
ワイ「まあ一例や」
ワイ「あと解説役が2人いるとやりやすいこともあるで」
娘「特に複数の話題がある場合や解説を後回しにする場合は、話題の担当者を明確にすることで読者の記憶を助ける効果もあると思うよ!」
ワイ「ほら、娘ちゃんは教師役さかい賢そうなしゃべり方なんや」
ハスケル子「賢そうというかなんか地の文に近くて堅いですね」
ワイ「まあ感覚で書いとる部分もあるんやけど、こういう所もキャラ付けに気を遣ったんやで」
ワイ「あとワイはレンダリングのことを『レンダリング』と呼ぶけど娘ちゃんは『描画』って呼んどるで」
ハスケル子「はあ」
ワイ「まあワイ記法も奥が深いってことを言いたかったんや」
ワイ「結構楽しいからみんなもワイ記法で記事書いてみてや!」
〜完〜