
テスト配布したアプリの本棚が消えた。スタイルの受け渡しを追った話
前回は、検査が通っていてもアプリが起動しなかった話を書きました。今回は、起動はするのに、本棚だけが表示されなかった話です。
TestFlight(テスト配布の仕組み)に出したアプリを実機で開いたら、本棚の画面に何も表示されていませんでした。
正確には、何もない黒い画面に、小さな点が2つだけ浮いている。
型検査もテストも通っていましたが、配布用のビルドで描画の不具合を見落としていました。何が起きていたのか、順番に書きます。
本棚は消えたのに、リストは表示される
積読アプリには、登録した本を背表紙が並んだ本棚として見せる画面があります。ここが完全に空でした。
一方、同じデータを使うリスト表示は正常。つまりデータは取れている。描画だけが壊れている。
そして「点が2つ」。これが不気味でした。背表紙が1本も見えないのに、点だけある。
小さな点だけが残った理由
先に答えを言うと、背表紙のスタイルが丸ごと消えていたんです。幅も高さも背景色も padding も。
今回の背表紙は、指定していた幅や高さを失い、意図した形で表示されなくなりました。
では点は何かというと、読書状態を表す 5×5 の丸で、自前の色を直接持っていた要素でした。共通スタイルが消えても、この色指定だけは別の場所にあったので生き残った。
さらに悪いことに、ダークテーマだと背表紙の文字色は #121212 。背景も #121212 。仮に文字が描かれていても同色で見えません。
結果、「真っ黒な画面に点が2つ」という、一見わけの分からない見た目になりました。
スタイルが、途中で別の形に変わっていた
コードはこう書いていました。
<Link asChild>
<Pressable style={[styles.spine, { width, backgroundColor }]}>React Native では、style に配列を渡せます。複数のスタイルを重ねる、ごく普通の書き方です。
問題は、今回使っていた <Link asChild> と、その先のスタイル処理の組み合わせでした。以下は当時の制作記録に基づく説明です。
expo-router の Link は asChild を指定すると、@radix-ui/react-slot の Slot を使って子要素を複製します。その中の mergeProps が、style をこう扱っていました。
// node_modules/@radix-ui/react-slot/dist/index.js
{ ...slotProps.style, ...childProps.style }オブジェクト展開です。
配列 [a, b] をオブジェクト展開すると、どうなるか。
{ ...[a, b] } // → { 0: a, 1: b }キーが 0 と 1 のオブジェクトになります。React Native から見ると、width も backgroundColor も存在しない、意味のないスタイルです。
だから全部消えました。
開発用のエラー表示は、本番では無効だった
ここが今回いちばん学びのあった部分です。
expo-router 側の Slot 実装には、こういうガードが入っていました。
if (process.env.NODE_ENV !== 'production') {
// 配列 style を検出して例外を投げる
}開発ビルドでは例外が出ます。 だから普通は気づけます。
しかし本番ビルドでは NODE_ENV === 'production' なので、このガードは丸ごと無効。例外は投げられず、何も起きずに描画だけが壊れます。
制作記録には、開発時に配列スタイルの例外に遭遇した記録も残っています。それでも、配布用ビルドの本棚で同種の問題を見落としていました。今回の表示不良は、実機のスクリーンショットをきっかけに調べています。
スタイルをひとつにまとめてから渡す
書き方をこう変えました。
<Link asChild>
<Pressable style={StyleSheet.flatten([styles.spine, { width, backgroundColor }])}>StyleSheet.flatten で、配列を1枚のオブジェクトに潰してから渡す。これならオブジェクト展開されても壊れません。
該当箇所はアプリ全体で9か所ありました。
同じ書き方をテストで検出する
直しただけでは、また同じことを書きます。なので、こういうテストを足しました。
app/ と src/ 配下の全 .tsx を静的に走査して、
<Link asChild> の直下の子に配列 style を渡している箇所があれば落ちる現在のテストは、該当する書き方が残っていたら失敗するようにしています。「テストがある」だけでなく、何を検査しているかが読める形にしました。
開発時の確認だけでは、見落とす動きがある
ライブラリのソースを読んでいて NODE_ENV !== 'production' のガードを見つけたら、裏側を読むべきです。
開発用の例外が、本番では無効になる場合があります。ただし、それだけで本番が壊れるとは限りません。今回は、例外が出なくなった先で、不適切な形のスタイルが渡されていました。
今回、この経路の不具合を既存の検査では捉えられませんでした。型は通る。lint も通る。テストも通る。バンドルも成功する。今回は、配布用ビルドを実機で確認するまで見落としていました。
表示の不具合から学んだこと
今回の構成では、<Link asChild> に渡す style を平坦化した。 同じ症状なら、利用バージョンとスタイルの受け渡しを確認する。
ライブラリの NODE_ENV 分岐を見つけたら、本番側で何が起きるかまで読む。 dev で投げる例外は、本番では起きない
再発防止テストでは、どの書き方を検出するのかを明確にする
次回:AIが本番データを調べる範囲
次回は、AIに本番データベースを見せて、起きていない障害を追いかけた話を書きます。技術というより、権限の話です。
本番だけ壊れた経験があれば、コメントで教えてください。