ReactNative知見

ReactNativeを使って開発を始めたので、ここに知見を溜めていく。
ちなみに筆者はウェブ系。React歴は長いけど、ReactNativeは初めて。ネイティブアプリ開発自体が初めて。

Unexpected text node エラー
React のノリで普通に開発していたら遭遇。
View の中にそのままテキストノード(文字列)をそのまま書いてはいけない。
// ❌️
const Name = () => {
return (
<View>Foo</View>
)
}
// ✅️
const Name = () => {
return (
<View>
<Text>Foo</Text>
</View>
)
}
Pressable の中も同様。テキストノードはとにかく Text で囲む。

実機用の余白は SafeAreaView で対応
何も考えずに開発し、いざ実機で見てみると、余白の設定が間違っていることに気づく。
画面の上部が、実機の時刻表示やバッテリー表示部分と重なってしまっている。
この問題に対応するには、 SafeAreaView を使う。
各スクリーンに適用する。コンポーネントツリーのルートを <View />
ではなく <SafeAreaView />
に置き換える。
style 指定をしつつ、必要な edges
を指定する。
<SafeAreaView style={styles.container} edges={["top", "left", "right"]}>

複数のカスタムフォントを同時に指定できない?
ウェブ開発では、複数のカスタムフォントを利用することがある。CSS指定は以下のような感じ。
p {
font-family: "MainFont, SubFont, DefaultFont";
}
左から順に評価され、マッチするフォントが適用される。例えば、英語では MainFont が適用され、日本語では SubFont が適用される、のような感じ。
しかし、ReactNative の <Text />
ではこの指定ができなかった。
単一の font-family 指定だと効いていのに、複数指定にしたらそもそもカスタムフォントが適用されなくなった。
調べてみても、そもそも複数のカスタムフォントを利用している例が少ない。挙句の果てには、システムフォントを使うのが無難ですという記事も見かけた。
というわけで、一般の文字はシステムフォントに任せることに…。装飾的な文字だけ個別にカスタムフォントを当てることにした。

AndroidのためのCSS指定
AndroidのためのCSS指定がいくつかある。 elevation
とか textVerticalAlign
とか。逆にこれらを指定しないと、Android端末で微妙に見た目が異なってしまう。

BottomTabNavigator でも SafeArea 考慮
BottomTabNavigator の下部が、実機のナビゲーションバーやインジケータと被っている。
こちらも、SafeArea の考慮を行う。
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
const Tab = createBottomTabNavigator();
export const MainNavigator = () => {
const insets = useSafeAreaInsets();
return (
<Tab.Navigator
screenOptions={{
tabBarStyle: {
height: 55 + insets.bottom,
paddingTop: 4, // 上部の余白もあっていい
paddingBottom: insets.bottom, // safe area 分の余白を設定
},
}}
>
{/* ここにタブ内容 */}
</Tab.Navigator>
);
};

Navigation 定義は静的に保つべき?
ReactNavigation のナビゲーション定義内では、あまり動的な記述はしないほうがいいかもしれない。
例えば以下のように定義する。
import { NavigationContainer } from "@react-navigation/native";
export function AppNavigator() {
// Contextなどから認証情報を取る
const { session } = useSession();
return (
<NavigationContainer>
{/* 認証情報に応じて、出し分ける */}
{session === null ? <AuthNavigator /> : <MainNavigator />}
</NavigationContainer>
);
}
すると、 AuthNavigator
と MainNavigator
の間では遷移ができなくなるっぽい。(たぶん)
Navigation 定義は以下のように静的にしておくのが良いかもしれない。こうすると、相互に遷移が可能になる。
import { NavigationContainer } from "@react-navigation/native";
export function AppNavigator() {
return (
<NavigationContainer>
<AuthNavigator />
<MainNavigator />
</NavigationContainer>
);
}

Supabase Storage にファイルアップロードできない
Supabase Storage にファイルアップロードできない問題が発生。
2日くらいハマった。AIでも解決できず。
顛末について、以下に書いた。
うーん、難しい。
localhostでの起動とExpoGoを含む実機での起動の環境差異がよく分かっていない。

Previewビルドの待ち時間
Previewビルドを何度かしてみた。すぐに終わるときもあるし、数十分かかるときもある。
開発が一段落したタイミングで、ひとまずpreviewビルドしてみるのが良いのかも。とにかくキューに入れておく。

KeyboardAvoidingView
OSのキーボードが表示されると、画面の下部が隠れてしまう。
ウェブだと画面全体が自動でスクロールアップされるが、ReactNativeだと自前で実装しないといけない。
以下のように KeyboardAvoidingView
を利用する。
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 20 : 0}
>
// ここにスクロールアップさせたい要素
</KeyboardAvoidingView>

スクロールには overflow:scroll ではなく ScrollView を使う
CSS の overflow:scroll
している箇所が、Android 実機でスクロールできなかった。iOSでは未確認だけど、Claude によると同様っぽい。
ScrollView を使うことで解決。
というか、スクロールさせたい箇所は ScrollView や FlatView を使うべき。overflow:scroll
を使う場面は無さそう。もはや linter で禁止にすべきかもしれない。

ステータスバーのテキストが見えない
SafeAreaView を使うことで、アプリの内容がステータスバーと被らなくなった。ステータスバーとは、時刻表示やバッテリー表示の部分。
しかし、別の問題が発生。アプリの背景色が白で、ステータスバーの文字色も白なので、時刻表示などが見えない。
この問題は以下のように解決。
// app.tsx
import { StatusBar } from "expo-status-bar";
// レンダー部分
<StatusBar style="dark" backgroundColor="#FFFFFF" translucent={false} />
dark
を指定することで、文字色が黒になる。

認証状態の保持
Supabase の認証状態を端末?に保持しておく方法。これをしないと、アプリを開くたびにサインインが必要になってしまう。
とは言え、SupabaseとReactNativeの組み合わせなら簡単。
まずは必要なパッケージのインストール。
npx expo install @react-native-async-storage/async-storage
次に、Supabase のクライアント初期化設定を修正する。
import { AppState } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createClient } from "@supabase/supabase-js";
import { Database } from "@/types/supabase";
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
// 以下の設定を追加!
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});
// こちらの設定も追加しておくと、勝手に認証状態のリフレッシュをしてくれる
AppState.addEventListener("change", (state) => {
if (state === "active") {
supabase.auth.startAutoRefresh();
} else {
supabase.auth.stopAutoRefresh();
}
});
以上。

カメラやファイルのアクセス権限
自分が作成しているアプリでは、カメラとファイルへのアクセスが必要。なので権限確認を実装する。
マイクや位置情報の権限は不要。
ちなみに、ここまでの開発では問題なかった。ローカル環境やプレビュービルドだと問題なかった。
しかしClaudeによると、本番ビルドでは問題になるらしい。
というわけで、最初から実装する必要はないけど、本番リリースの前にはやるべき事項だ。

expo-image-picker vs expo-camera
ReactNative でカメラ機能を使う場合、expo-image-picker と expo-camera の2つの選択肢がある。
前者はシンプルで、後者は複雑なことまでできるらしい。
シンプルな用途の場合、expo-image-picker で十分っぽい。

本番ビルド確認
アプリの公開前に本番ビルドを試しておく。
プレビュービルドまでは問題なかったのに、本番ビルドでは落ちた!みたいなことはあるらしい。
本番ビルドの流れについて、以下の記事に書いた。1発で成功して良かった…!

バックグラウンドからの復帰検知
AppState を使えば、フォアグラウンドとかバックグラウンドとか、そういうのが検知できるらしい。
めちゃくちゃ大事な知見だ…!

アプリのバージョン更新
app.json で versionCode を変更しても Expo 上で反映されなかった。
これは esa.json で cli.appVersionSource: "remote"
となっているためだった。新しいバージョンでは、remote が標準っぽい?
このとき、app.json の versionCode は無視される。
esa.json で build.production.autoIncrement: true
を指定することで解決。
公式ドキュメントのこのあたりに書いてある。

Expoビルド前の処理
Expoでビルドする前に、何かしらの処理をしたいことはあると思う。
自分の場合、環境変数がちゃんと設定されているかチェックしたかった。そのスクリプトを実行する方法。
eas-build-pre-install
コマンドを使う。 package.json の scripts に追加すればOK。

音声再生
expo-audio
パッケージを使う。 expo-av
パッケージは deprecated なので注意。