メインコンテンツへスキップ
見出し画像

テスト配布したアプリの本棚が消えた。スタイルの受け渡しを追った話


    前回は、検査が通っていてもアプリが起動しなかった話を書きました。今回は、起動はするのに、本棚だけが表示されなかった話です。
    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に本番データベースを見せて、起きていない障害を追いかけた話を書きます。技術というより、権限の話です。

    本番だけ壊れた経験があれば、コメントで教えてください。

    あなたへのおすすめ