個人開発・写真アルバムアプリ開発ログ・メモ
メモも兼ねて今開発している写真アルバムアプリの開発ログを書いていく。
一応個人メモのつもりではあるが、自分と同じところで引っかかった・ハマった誰かの役に立てるように自分がハマったところとどう対処したかも書き残していくつもり。
これを書き始めた10月24日時点で完成度は3-4割くらい。
構成
変わる可能性あり。
- Remix (React + TypeScript)
- ガッツリ Cloudflare (Pages, Workers, R2, D1, etc...)
- コンポーネント ―― 基本的には React Aria Component OR 自作。
- スタイリング ―― Lightning CSS で CSS Modules。
- デザイン ―― モバイル向けだし Google の Material Design 3。
- 想定ユーザー数 ―― 自分を含めて2名。プライベート用。
コンポーネント
以前からアクセシビリティについて興味があったこと、用意されているコンポーネントの種類が豊富なことから React Aria Component をメインに選んだ。
Material Design なのに MUI を選ばなかったのは自分でガッツリスタイリングしたかった・スタイリングの自由度が高い Headless なコンポーネントが欲しかったから。
React Aria Component (以下 RAC)
すごい(小学生並みの感想)。
開発しているのが Adobe なだけあって大企業・グローバル企業に求められるクオリティを満たすように作られている。
ただ、流石に想定ユーザー数2名の個人開発には少しやりすぎ感を感じなくもない。
一応 RAC のコンポーネントではなく React Aria の hooks から必要な機能だけを抜き出してコンポーネントを作ることもできる。
RAC は単純に React Aria にガワを用意しただけなのかというとそうではなく、ちゃんとしたコンポーネントライブラリとして作られている。
同じ RAC の他コンポーネントと自動で連携して機能するように作られていたり、同じ Adobe 製の React Spectrum ライブラリと一緒に使うことを想定した作りになっていたりと、軽い気持ちで踏み入れるといつの間にか React Spectrum の沼にどっぷり浸かることになる。
とはいえ便利なものばかりなので不満はない。タイムゾーンとかが関わってくる Internationalized だけはつらみがあるけど。
スタイリング
最初は Vanilla Extract で開発していたが、最近活発に追加されている CSS の新機能に追いつけていない点がつらくて移行。
移行先の選択肢として Panda CSS なども考えたが、フレームワークを使う為の新しい構文を覚えるのが正直面倒に感じたのと Vite で使える Lightning CSS に興味があったので Lightning CSS (CSS Modules) に。
自由度がほしいので Tailwind CSS は除外。以前使ったことあるし。
Lightning CSS (CSS Modules)
いいっすね(中並感)。
Vanilla Extract でも基本的にはあまり困っていなかったが、そのまんま素の CSS で書けるのも結構快適。
強いて言うならセレクタの import / export が出来ないので他のコンポーネントと組み合わせたセレクタ (.comp-a > .comp-b
みたいな) が書けないことだけは残念。でもなんとかなる。
ただの CSS ファイルなので CSS 用の拡張機能が使えることやコピペで使う Web ツール使用時に変換しなくていいのがありがたい。
でもセミコロンめんどくさい。CSS in JS で書く時に ""
で value を囲むのは苦じゃないんだけど。
デザイン
スマホはずっと Android ユーザーなのと、ああいったモバイルアプリ的なアニメーションが結構好きなので Material Design を選択。
公式ドキュメントにデザインの詳細が詳しく書かれていることや、複数の言語・環境用実装コードが公開されているのもうれしい。
詳しくはたぶんあとで書く。
ハマったとこ
input[type="file"]
Conform と とりあえず Conform を試してみようと思って使っていたが、どうやら [type="file"] な input にはまだ完全に対応していないようでところどころハマるポイントがあった。開発が落ち着いたら PR を出したい。
とくに「並び替え・入れ替え等のファイル数が変わらない変更」を検知してくれない不具合には少し手こずった。
原因は「変更確認に使っている JSON.stringify()
を File に対して実行するとすべて同じ {}
になってしまうから」だった。
Object.assign()
とか File を extend したクラスで toJSON()
を追加して解決。JSON.stringify()
実行時にターゲットが toJSON()
メソッドを持っていた時にはこれが使用されるので、ファイル名などを返り値に含めばファイルを区別できる。被っていなければ。
例:
toJSON() {
return { "[object File]": this.name }
}
context.cloudflare.env
の型がつかない
typegen
(wrangler types
) を実行しても型がつかなかった。
試しに load-context.ts にある空の Env 型定義をコメントアウトしてみたら binding の型がちゃんとつくようになった。謎。
"Provided readable stream must have a known length" @R2
Workers はメモリ容量の制限があるので、FormData を stream で読み込むライブラリを使用して送られてきた File を R2 に流し込もうとしたら上記のエラーが出た。
Workers の API に FixedLengthStream というものが用意されており、それを使いなさいということのようだがファイルサイズ取得については自分でどうにかやるしかない模様。
仕方ないので隠しフォームからファイルのサイズを格納した配列、をテキスト化したやつを送るようにして解決。
pipeTo(...)
=> "This WritableStream has been closed"
さあ FixedLengthStream を使おう、と思ったらこれが出た。
まだ本番環境 (Workers) では試していないが、少なくとも wrangler pages dev
で起動する開発サーバでは ReadableStream を pipeTo()
や pipeThrough()
で繋げるとこのエラーになる不具合があるようだ。
とりあえず手動で書き写して解決。
結局 conform では少しつらみがあったので Modular Forms に切り替えてみた。
基本的に満足だけどこちらも少しクセがある。でも大体なんとかなるのでつらみは少ない。
Modular Forms を使ってみた時にハマった点と対処法を書いていく。
Signal を使う準備をする(Remix)
このライブラリは内部で @preact/signals を利用しているのだが、React で使うには先におまじないをする必要がある。公式ページに載っている二種類と載っていないもの一種類の計三種類ある。
- babel プラグインを入れる
- 公式で推奨されているやり方。しかし Remix の Vite プラグインには babel プラグインを扱うオプションが無い。自分で入れればいいのだが先に他の方法を試そうと思ってとりあえずスキップ。
-
useSignals
を使う- Signal を使いたいコンポーネントの中で
useSignals()
とやるだけで期待通り動作してくれるようになる。
のだが、内部でuseLayoutEffect
を使用しているようで、いつもの「SSR 時に使うな」みたいな警告が出てきてしまうようになったのでやめた。
- Signal を使いたいコンポーネントの中で
-
@preact/signals-react/auto
を import する- 公式 readme に載ってないやつ。Modular Forms の Playground のソースコードに載っていたのを見て知った。
これを root ファイルで import するだけで問題なく使えるようになる。
これが一番楽そうに思えたので採用。今のところ問題はない。
できたら上2つのやり方にしたほうがいいのはわかっているけども…。
- 公式 readme に載ってないやつ。Modular Forms の Playground のソースコードに載っていたのを見て知った。
ネストされたプロパティを含む FormData の値を検証する
Modular Forms では conform と同じく { outer: { inner: {}, array: [] } }
のような階層がある型を使うことができる。
FormData に変換した時の key は outer.inner
というようなフラットな形になっている。
ただ、Modular Forms では conform とは違いこの FormData を元のオブジェクトに戻す為のメソッドが用意されていないっぽい。
ただただ、調べてみたところこの問題を解決する為のライブラリを Modular Forms の開発者さんが公開していた。ありがたや。
サンプルコードに書いてある通り、一度 FormData に変換されると File 以外の全て?の値が string になってしまう為、decode()
の引数に型のヒントを与えてやる必要がある。
配列のインデックスは array.$.item
みたいに $
を使って表現することだけ気をつければ簡単に使える。
input[type='file']
の value を動的に変更する
Modular Forms は input[type='file']
の value を変更するのは対応していない模様。
ならば普通に ref を使って HTMLInputElement.files から読み書きしようと思ったが、Modular Forms から渡される props に ref が含まれている & その ref はいつもの { current: Element | null }
ではなく関数タイプ、という触りにくい(?)仕様だった。
しかしありがたいことに上記の adobe 製 React Aria ライブラリにいつもの ref に変換してくれるユーティリティが用意されている。
forwardRef で受け取った ref を一度この useObjectRef に渡せばいつものものに変えてくれる。
やりたい処理をやった後は変換後の ref をそのまま対象の要素に渡すだけでいい。便利。
ちなみに、複数個のファイルを設定したい場合は FileList というインスタンスを作成する必要があるのだが、このインスタンスにはコンストラクタが無い。
代わりに DataTransfer を作成してそこから FileList を取得する必要がある。