D言語くんと秘密のレポート2


はじめに

さて、今年もD言語くんアドベントカレンダーが始まりました!

昨年に引き続き、 @simd_nyan 氏によって1日目に公開されたD言語くんのツイート一覧を見るために独自のビューアーを作っていますが、こちらをより扱いやすくアップデートしていきます。

大阪万博のロゴで脚光を浴びることもありましたが、VRChatへの参戦やイラスト投稿など継続的に発展を遂げており、
そんな細かいことはいいんだよ!というD言語くんファンの方は以下URLをご覧ください。

今回からは自動再生もあるので、垂れ流しインお布団のスタイルもおすすめです。(寝てください)

ちなみに昨年の記事は以下になります。

概要

さて、今回も技術的には React + TypeScript + Material-UIです。

Vueも3.0になったし別に作ろうかと考えましたが時間不十分と判断して断念しました。
雑に作るのはD言語くんに失礼なので仕方ありません。

というわけで更新しました。以下のような感じです。

更新点

前回からの変更点は以下3つです。

  • 総件数表示
  • 自動再生
  • ツイート読み込み中および読み込み失敗表示の追加
  • インクリメント周りのバグ修正

総件数表示は文字通り件数表示しただけなので割愛します。
自動再生と読み込み中表示と読み込み失敗表示について軽く説明していきます。

読み込み中表示

Twitter公式で提供されるスクリプトを使えば埋め込みツイートは簡単に表示できます。
細かいコードは昨年記事を参照してください。

変えた部分は、Reactの useRef を使って適当なdivに埋め込む以下のコードのあたりです。

const t = twttr.widgets.createTweet(tweet, containerRef.current, {});

createTweet の戻り値は Promise<HTMLElement> となっているのですが、これの完了を待機することで読み込み完了が判定できます。

読み込めないツイートに関しては完了後にノードの中が空になるため、それを使って判定します。(catch ではない点に注意)

まず表示部のイメージですが、エラーフラグなど見ながら表示を変えること、コンテナにあたるノードは常に置いておくことの2点に留意して以下のようになります。

return <>
    <div key="container" ref={containerRef}></div>
    {loading && (<div>Loading...</div>)}
    {error && (<div>Not Found</div>)}
</>;

あとはフラグを用意して、Promiseの完了を待って切り替えればOKです。

const t = twttr.widgets.createTweet(tweet, containerRef.current, { width: Math.min(window.innerWidth - 20, 500) });
// 追加
setLoading(true);
setError(false);
t.then(() => { 
    setLoading(false);
    setError(!containerRef.current.firstChild);
});

return () => {
    t.then(e => {
        if (e && e.parentNode) {
            e.parentNode.removeChild(e);
        }
    });
};

自動再生

自動再生は、再生ボタンを押すと2秒待機して次に進む動作を行い、読み込みが終わったら待機を繰り返します。

何も考えず setInterval などをコールバックで呼べば簡単なのですが、せっかくなので読み込みを待って適切に処理しつつ、副作用は useEffect に収まるよう少し手間をかけました。
useReducer を使ってやるところですが、管理がちょっと面倒だったのでベタ書きしてしまいまったのは反省)

ポイントとしては、自動再生用フラグ以外にもう1つフラグ用意しておき、任意のタイミングで useEffect をトリガーできるようにする点です。
フラグはある程度隠蔽したいので、独自のHooksということで useSignal というのを作ります。

useSignal
function useSignal(): [boolean, () => void] {
    const [signalCount, setSignalCount] = React.useState(false);
    const notify = React.useCallback(() => { setSignalCount(current => !current); }, [setSignalCount]);
    return [signalCount, notify];
}

これを使って必要な状態やコールバックを準備します。

状態
const [autoPlay, setAutoPlay] = React.useState(false);
const [tweetLoaded, notify] = useSignal(); // 独自のHooks

const handleTweetLoaded = React.useCallback(() => { notify(); }, [notify]);

一定時間待って更新するあたりを用意します。(見るからに useReducer にすべき感じですが、、)

クリック処理のエミュレート
React.useEffect(()=>{
    if (autoPlay) {
        const timer = setTimeout(() => {
            setCount(c => Math.min(c + 1, tweets.length - 1));
        }, AUTOPLAY_INTERVAL);
        return () => {
            clearTimeout(timer);
        };
    }
}, [autoPlay, tweetLoaded, tweets]); // ここにトリガー変数を入れておくと任意のタイミングで発火できる

あとはロード時にトリガー変数を更新するハンドラを設定しておけば、 useEffect の依存変数が切り替わることでuseEffect の処理がトリガーされます。

onLoadでトリガー
<Tweet tweet={tweets[count]} onLoad={handleTweetLoaded} />

おわりに

useStateuseEffect を使ったプログラミングにもだいぶ慣れてきた感じですが、自動再生あたりになってくるとやや入り組んできた感じになりますね。

この際 ContextuseReducer など使ってしっかり書いておくべきというのはあるのですが、時間足らず今回はここまでとします。

それではみなさんもD言語くんと同じ視点に立って、この年末も無事に過ごしてまいりましょう!