React Native(Expo)アプリでAPIを呼び出したりデータベースに登録処理を行ったりする際、通信が完了するまでの数百ミリ秒〜数秒間、ユーザーには「今何が起きているのか」が見えません。この「間」を放置すると、ユーザーがボタンを連打してしまったり、アプリが固まったと勘違いして離脱してしまったりする原因になります。
この記事では、React Native + Expo環境で再利用可能なローディングオーバーレイコンポーネントを実装し、実際のフォーム画面(食品登録アプリを例に)に組み込む方法を、コード付きで解説します。
この記事でわかること
- ローディング表示がUXに与える影響
ModalとActivityIndicatorを使った全画面ローディングオーバーレイの作り方Animatedによるフェードイン/アウトの実装- 既存の非同期処理(DB登録・API通信)への組み込み方
- 複数画面で使い回すための設計のコツ
なぜローディング表示が必要なのか
モバイルアプリの体感速度は、実際の処理時間だけでなく「フィードバックの有無」によって大きく左右されます。特に以下のようなケースでは、ローディング表示がないとユーザー体験が著しく損なわれます。
- 通信が数秒かかる処理(外部API呼び出し、DB書き込みなど)
- ボタンの連打による二重送信のリスクがある処理
- 画面遷移を伴う保存処理(保存が完了する前に戻ってしまう可能性がある)
単純に「ボタンを無効化する」だけでも二重送信は防げますが、視覚的なフィードバックがなければユーザーは「反応していない」と感じてタップを繰り返してしまいます。そこで有効なのが、画面全体を覆うローディングオーバーレイです。
実装方針
今回は以下の方針でコンポーネントを設計します。
Modalコンポーネントを使う:position: absoluteで自作するより、画面全体に確実に重ねて表示できるvisiblepropで表示/非表示を制御:呼び出し側のstateと連動させるだけで使えるAnimatedでフェードイン/アウト:表示・非表示の切り替えを滑らかにし、チラつきを防ぐ- Android・iOS両対応:
onRequestCloseでAndroidの戻るボタン挙動も制御
LoadingOverlayコンポーネントの実装
まずは再利用可能なコンポーネントとして切り出します。src/components/LoadingOverlay.tsx を新規作成します。
tsx
// src/components/LoadingOverlay.tsx
import React, { useEffect, useRef } from "react";
import {
View,
Text,
StyleSheet,
ActivityIndicator,
Modal,
Animated,
} from "react-native";
interface LoadingOverlayProps {
visible: boolean;
message?: string;
}
const COLORS = {
accent: "#52B788",
card: "#FFFFFF",
text: "#1A1A2E",
};
export default function LoadingOverlay({
visible,
message = "登録中...",
}: LoadingOverlayProps) {
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: visible ? 1 : 0,
duration: 180,
useNativeDriver: true,
}).start();
}, [visible, fadeAnim]);
return (
<Modal
transparent
visible={visible}
animationType="none"
statusBarTranslucent
onRequestClose={() => {}}
>
<Animated.View style={[styles.backdrop, { opacity: fadeAnim }]}>
<View style={styles.card}>
<ActivityIndicator size="large" color={COLORS.accent} />
<Text style={styles.text}>{message}</Text>
</View>
</Animated.View>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.4)",
alignItems: "center",
justifyContent: "center",
},
card: {
backgroundColor: COLORS.card,
borderRadius: 16,
paddingHorizontal: 32,
paddingVertical: 28,
alignItems: "center",
gap: 14,
minWidth: 160,
elevation: 6,
},
text: { fontSize: 14, fontWeight: "700", color: COLORS.text },
});
実装のポイント
なぜ View の絶対配置ではなく Modal を使うのか
position: absolute + StyleSheet.absoluteFill で自作する方法もありますが、ScrollView や KeyboardAvoidingView のレイアウト階層に影響を受けやすく、意図せず要素の裏に隠れてしまうことがあります。Modal はネイティブレイヤーでルート上に描画されるため、階層を気にせず確実に最前面に表示できます。
onRequestClose={() => {}} の役割
Androidでは物理・ジェスチャーの「戻る」操作で Modal が閉じられてしまうことがあります。DB登録処理中に誤って画面遷移されると不整合の原因になるため、空の関数を渡して無効化しています。
Animated によるフェード
visible の切り替えを即座に反映すると、一瞬だけローディングが表示されてすぐ消えるような処理(通信が数十ミリ秒で終わる場合など)でチラつきが発生します。180msのフェードを挟むことで、視覚的な違和感を軽減しています。
既存の非同期処理への組み込み
続いて、実際にAPI通信(DB登録)を行っている画面に組み込みます。多くの場合、非同期処理の前後で saving のようなbooleanのstateをすでに管理しているはずです。
tsx
const [saving, setSaving] = useState(false);
const handleSave = async () => {
setSaving(true);
try {
await addProduct(barcode, name.trim());
} catch (error) {
console.error("登録に失敗しました:", error);
}
setSaving(false);
navigation.goBack();
};
このstateをそのまま LoadingOverlay に渡すだけで実装が完了します。
tsx
import LoadingOverlay from "../components/LoadingOverlay";
// ...
return (
<KeyboardAvoidingView style={styles.container}>
{/* 既存のフォームUI */}
<LoadingOverlay visible={saving} message="登録中..." />
</KeyboardAvoidingView>
);
追加で行う実装は最小限で済みます。
- 新しいstateを増やす必要がない(既存の
savingを流用) - ボタンの
disabled={saving}と組み合わせれば二重送信も防止できる - ボタンのラベルを
{saving ? "登録中..." : "保存する"}のように出し分けるとさらに親切
複数画面での使い回し方
ローディングオーバーレイは1つの画面だけでなく、DB通信や外部API通信が発生するあらゆる画面で共通して使えます。設計のポイントは以下の3つです。
- コンポーネントを
src/components/に切り出す:画面ごとに実装を重複させない messagepropを外部化する:「登録中…」「削除中…」「更新中…」など文言だけ変えて使い回す- stateの命名を統一する:
saving/loading/submittingなど、プロジェクト内で命名規則を決めておくと保守性が上がる
例えば削除処理でも同様に使えます。
tsx
const [deleting, setDeleting] = useState(false);
const handleDelete = async () => {
setDeleting(true);
await removeItemFromServer(id);
setDeleting(false);
};
// ...
<LoadingOverlay visible={deleting} message="削除中..." />
まとめ
React Native(Expo)でのローディングオーバーレイ実装は、以下の3ステップで完結します。
Modal+ActivityIndicator+Animatedで再利用可能なコンポーネントを作る- 既存の非同期処理の
savingなどのstateをそのままvisiblepropに渡す - 複数画面で使い回せるよう
messagepropで文言だけ切り替える
わずか数十行のコンポーネントですが、DB登録やAPI通信を伴うすべての画面のUXを底上げできる、投資対効果の高い実装です。特にモバイルアプリでは通信環境によって待機時間が伸びることもあるため、こうした「待たせている間の安心感」を作り込むことがユーザー満足度に直結します。
よくある質問(FAQ)
Q. ActivityIndicator の代わりにLottieアニメーションを使ってもいい?
A. 可能です。lottie-react-native を導入し、ActivityIndicator を差し替えるだけで同じ設計のまま高品質なアニメーションに変更できます。
Q. 通信が一瞬で終わる場合もローディングを出すべき?
A. 数百ミリ秒未満で終わる処理では、オーバーレイの表示自体がチラつきに感じられることがあります。今回のようにフェードで緩和するか、一定時間(例:300ms)以上かかった場合のみ表示する遅延ロジックを追加するのも有効です。
Q. iOSとAndroidで挙動に差は出ませんか?
A. Modal はプラットフォームごとにネイティブ実装が異なりますが、transparent と statusBarTranslucent を指定することで、両OSともにステータスバーを含めた画面全体を覆う表示に統一できます。


コメント