FlatList onRefreshだけじゃない!React Nativeリスト更新の代替手段と実装例
onRefresh
とは?
onRefresh
は、FlatList
コンポーネントに渡すことができるコールバック関数です。ユーザーがリストの一番上までスクロールし、さらに下に引っ張るジェスチャー(プルツーリフレッシュ)を行ったときに、この関数が呼び出されます。
この機能は、ニュースフィードやメールボックスなど、最新のデータを取得して表示したい場面で非常に便利です。
どのように使うのか?
onRefresh
を使用するには、通常、以下の2つのプロパティをFlatList
に設定します。
onRefresh
: プルツーリフレッシュジェスチャーが行われたときに実行される関数を指定します。この関数内で、新しいデータを取得するための処理(API呼び出しなど)を実行します。refreshing
: 現在データが更新中かどうかを示すブール値の状態(state)を指定します。データ取得中はtrue
に設定し、取得が完了したらfalse
に戻します。これにより、リストの上部にローディングインジケーター(くるくる回るアイコンなど)が表示され、ユーザーに更新中であることを知らせることができます。
一般的な実装の流れ
- 状態の定義:
refreshing
の状態を管理するために、useState
フックなどを使用して状態変数を定義します。初期値はfalse
です。const [refreshing, setRefreshing] = useState(false);
onRefresh
関数の作成: この関数内で、以下の処理を行います。setRefreshing(true)
を呼び出し、ローディングインジケーターを表示させます。- APIから新しいデータを取得するなどの非同期処理を実行します。
- データ取得が完了したら、
setRefreshing(false)
を呼び出し、ローディングインジケーターを非表示にします。
const onRefresh = useCallback(async () => { setRefreshing(true); // 更新開始 // ここでデータを取得する処理(例: API呼び出し) try { const newData = await fetchDataFromApi(); // 実際のデータ取得 setData(newData); // データを更新 } catch (error) { console.error("データの取得に失敗しました:", error); } finally { setRefreshing(false); // 更新完了 } }, []);
useCallback
は、onRefresh
関数が不要に再作成されるのを防ぐために使用されることがよくあります。FlatList
へのプロパティ設定:<FlatList data={yourData} renderItem={({ item }) => <Text>{item.name}</Text>} keyExtractor={(item) => item.id} refreshing={refreshing} // 更新中かどうか onRefresh={onRefresh} // プルツーリフレッシュ時のコールバック />
import React, { useState, useCallback, useEffect } from 'react';
import { FlatList, Text, View, StyleSheet, ActivityIndicator } from 'react-native';
const MyAwesomeList = () => {
const [data, setData] = useState([]);
const [refreshing, setRefreshing] = useState(false);
// ダミーデータ取得関数 (非同期処理をシミュレート)
const fetchData = async () => {
return new Promise(resolve => {
setTimeout(() => {
const newItems = Array.from({ length: 5 }, (_, i) => ({
id: `${Date.now()}-${Math.random()}-${i}`,
title: `新しいアイテム ${Date.now()}-${i}`
}));
resolve([...newItems, ...data]); // 新しいアイテムを既存のデータに追加
}, 1500); // 1.5秒の遅延をシミュレート
});
};
useEffect(() => {
// コンポーネントがマウントされたときに初回データをロード
const loadInitialData = async () => {
setRefreshing(true);
const initialData = await new Promise(resolve => {
setTimeout(() => {
resolve(Array.from({ length: 10 }, (_, i) => ({
id: `${Date.now()}-initial-${i}`,
title: `初期アイテム ${i + 1}`
})));
}, 1000);
});
setData(initialData);
setRefreshing(false);
};
loadInitialData();
}, []);
// プルツーリフレッシュ時に呼び出される関数
const handleRefresh = useCallback(async () => {
setRefreshing(true); // ローディングインジケーター表示
const newData = await fetchData(); // 新しいデータを取得
setData(newData); // データを更新
setRefreshing(false); // ローディングインジケーター非表示
}, [data]); // dataが変更されたらhandleRefreshも再作成
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
refreshing={refreshing} // refreshingの状態を渡す
onRefresh={handleRefresh} // プルツーリフレッシュ時の関数を渡す
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
{refreshing ? <ActivityIndicator size="large" /> : <Text>データがありません。</Text>}
</View>
)}
contentContainerStyle={data.length === 0 && !refreshing ? styles.centerEmpty : null}
/>
);
};
const styles = StyleSheet.create({
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 5,
},
title: {
fontSize: 18,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
centerEmpty: {
flexGrow: 1, // ListEmptyComponentを中央に配置するために必要
justifyContent: 'center',
},
});
export default MyAwesomeList;
この例では、useState
でdata
とrefreshing
という2つの状態を管理しています。handleRefresh
関数が実際にデータを取得する処理をシミュレートし、refreshing
の状態を適切に更新することで、ユーザー体験を向上させています。
onRefresh が呼び出されない / プルツーリフレッシュが動作しない
考えられる原因
-
FlatList に十分な高さがない
FlatList
が親コンポーネントによって高さが制限されている場合、スクロール可能領域が狭くなり、プルツーリフレッシュが困難になることがあります。- トラブルシューティング
FlatList
またはその親コンポーネントにflex: 1
などを適用して、利用可能な領域を最大限に占有するようにしてみてください。
- トラブルシューティング
-
リストのデータが少なすぎる
表示されているアイテムが少なく、画面全体に収まってしまう場合、ユーザーが下に引っ張る余地がないため、プルツーリフレッシュのジェスチャーが認識されないことがあります。- トラブルシューティング
データを一時的に増やして、画面外にはみ出すようにして試してみてください。本番環境では、データがない場合でも更新できるようにUIを考慮する必要があります(例: "データがありません。プルして更新"のようなメッセージを表示)。
- トラブルシューティング
-
FlatList が ScrollView の中にネストされている
FlatList
自体がスクロール機能を持っているため、これをScrollView
の中にネストすると、スクロールの競合が発生し、onRefresh
が正しく動作しないことがあります。- トラブルシューティング
FlatList
をScrollView
の中にネストするのを避けてください。もし複数のリストや他の要素をスクロールさせたい場合は、FlatList
のListHeaderComponent
やListFooterComponent
を使って、リストのコンテンツとして他の要素を含めることを検討してください。
- トラブルシューティング
-
refreshing プロパティの設定漏れまたは誤り
onRefresh
を機能させるには、refreshing
プロパティも同時に設定する必要があります。refreshing
が常にfalse
のままだと、FlatList
は更新中ではないと判断し、プルツーリフレッシュのジェスチャーを認識しないことがあります。- トラブルシューティング
refreshing
ステートが正しく定義され、onRefresh
が呼び出されたときにtrue
になり、データ取得後にfalse
に戻るように設定されているか確認してください。
const [refreshing, setRefreshing] = useState(false); // 必ずfalseで初期化 const onRefresh = useCallback(async () => { setRefreshing(true); // ★ここに注意 // データ取得処理 setRefreshing(false); // ★ここに注意 }, []); <FlatList // ...他のプロパティ refreshing={refreshing} // ★これも必要 onRefresh={onRefresh} />
- トラブルシューティング
onRefresh が一瞬で終わってしまう / ローディングインジケーターが見えない
考えられる原因
- データ取得処理が速すぎる
非同期処理(API呼び出しなど)が非常に高速で、setRefreshing(true)
が呼び出されてすぐにsetRefreshing(false)
が呼び出されてしまうと、ローディングインジケーターがほとんど表示されないことがあります。- トラブルシューティング
実際のアプリケーションでは、ネットワーク遅延などがあるため、ほとんど問題になりませんが、開発中にシミュレーションを行う場合は、setTimeout
などで意図的に遅延を入れると確認しやすくなります。
const onRefresh = useCallback(async () => { setRefreshing(true); await new Promise(resolve => setTimeout(resolve, 1000)); // 1秒間遅延 // データ取得処理 setRefreshing(false); }, []);
- トラブルシューティング
データが更新されない / 古いデータが表示される
考えられる原因
-
useCallback の依存配列の誤り
onRefresh
関数がuseCallback
でラップされている場合、その依存配列に古いdata
ステートが含まれていると、onRefresh
が実行されても古いdata
を参照してしまう可能性があります。- トラブルシューティング
useCallback
の依存配列に、onRefresh
内で利用するステートやプロパティがすべて含まれていることを確認してください。もしdata
を更新する関数(setData
)しか使わないのであれば、依存配列は空で問題ありませんが、data
自体を参照して更新するロジック(例:[...newData, ...data]
のように既存データに追加する場合)であれば、data
を依存配列に含める必要があります。
// 例えば、新しいデータを既存のデータに追加する場合 const onRefresh = useCallback(async () => { setRefreshing(true); const newItems = await fetchData(); setData(prevData => [...newItems, ...prevData]); // prevDataを利用 setRefreshing(false); }, []); // prevDataを利用する場合、dataを依存配列に入れる必要はない
または
const onRefresh = useCallback(async () => { setRefreshing(true); const newItems = await fetchData(); setData([...newItems, ...data]); // dataを参照 setRefreshing(false); }, [data]); // dataを依存配列に入れる必要がある
- トラブルシューティング
-
onRefresh 関数内でデータを更新していない
onRefresh
が呼び出されても、実際に表示するデータ(data
ステートなど)を更新する処理が抜けているか、誤っている可能性があります。- トラブルシューティング
onRefresh
関数内で、新しく取得したデータをFlatList
のdata
プロパティにバインドされているステート変数にセットしているか確認してください。
const [data, setData] = useState([]); const onRefresh = useCallback(async () => { setRefreshing(true); const newData = await fetchData(); // ★新しいデータを取得 setData(newData); // ★取得したデータでステートを更新 setRefreshing(false); }, []);
- トラブルシューティング
RefreshControl を直接使用しようとしてしまう
考えられる原因
FlatList
は内部的にRefreshControl
をラップして使用しているため、通常はonRefresh
とrefreshing
プロパティを直接FlatList
に渡すだけで十分です。誤ってFlatList
内に自分でRefreshControl
コンポーネントを配置しようとすると、期待通りに動作しないか、重複して表示されることがあります。- トラブルシューティング
FlatList
に直接refreshing
とonRefresh
プロパティを渡すことを確認してください。自分でRefreshControl
を使う必要があるのは、ScrollView
などでプルツーリフレッシュを実装したい場合です。
- トラブルシューティング
Androidでリフレッシュインジケーターの色がおかしい / カスタマイズしたい
考えられる原因
- デフォルトの色がアプリのデザインに合わない場合。
- トラブルシューティング
RefreshControl
のcolors
プロパティを使って色を指定できます(Androidのみ)。FlatList
に直接渡すのではなく、refreshControl
プロパティを使ってRefreshControl
コンポーネントを明示的に渡す必要があります。
import { FlatList, RefreshControl } from 'react-native'; // ... <FlatList // ... refreshControl={ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#9Bd35A', '#689F38']} // Androidでのみ有効な色 tintColor="#9Bd35A" // iOSでのローディングインジケーターの色 /> } />
- トラブルシューティング
FlatList
の onRefresh
プロパティは、ユーザーがリストをプルダウン(下に引っ張る)したときにデータを更新するための機能(プルツーリフレッシュ)を実装するために使われます。この機能は、refreshing
という状態(state)と組み合わせて使用するのが一般的です。
基本的なプルツーリフレッシュの実装
最もシンプルなプルツーリフレッシュの実装例です。
import React, { useState, useCallback, useEffect } from 'react';
import { FlatList, Text, View, StyleSheet, ActivityIndicator } from 'react-native';
const BasicRefreshExample = () => {
const [data, setData] = useState([]);
const [refreshing, setRefreshing] = useState(false); // 更新中かどうかを示すstate
// ダミーデータ取得関数 (非同期処理をシミュレート)
const fetchData = async () => {
return new Promise(resolve => {
setTimeout(() => {
// 現在のタイムスタンプを含む新しいアイテムを5つ生成
const newItems = Array.from({ length: 5 }, (_, i) => ({
id: `${Date.now()}-${Math.random()}-${i}`,
title: `新しいアイテム ${Date.now()}-${i}`
}));
// 既存のデータの先頭に新しいアイテムを追加
resolve([...newItems, ...data]);
}, 1500); // 1.5秒の遅延をシミュレート
});
};
// コンポーネントがマウントされたときに初回データをロード
useEffect(() => {
const loadInitialData = async () => {
setRefreshing(true); // 初期ロード開始時にもインジケーターを表示
const initialData = await new Promise(resolve => {
setTimeout(() => {
resolve(Array.from({ length: 10 }, (_, i) => ({
id: `${Date.now()}-initial-${i}`,
title: `初期アイテム ${i + 1}`
})));
}, 1000);
});
setData(initialData);
setRefreshing(false); // 初期ロード完了
};
loadInitialData();
}, []); // 空の依存配列でマウント時のみ実行
// プルツーリフレッシュ時に呼び出される関数
const handleRefresh = useCallback(async () => {
setRefreshing(true); // 更新開始 -> ローディングインジケーター表示
const newData = await fetchData(); // 新しいデータを取得
setData(newData); // データを更新
setRefreshing(false); // 更新完了 -> ローディングインジケーター非表示
}, [data]); // `data` を参照しているので依存配列に含める
// 各リストアイテムのレンダリング
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
refreshing={refreshing} // `refreshing` state を `FlatList` に渡す
onRefresh={handleRefresh} // プルツーリフレッシュ時に実行される関数を渡す
ListEmptyComponent={() => ( // データが空の場合の表示
<View style={styles.emptyContainer}>
{refreshing ? <ActivityIndicator size="large" /> : <Text>データがありません。</Text>}
</View>
)}
// データがない時にListEmptyComponentを中央に表示するためのスタイル
contentContainerStyle={data.length === 0 && !refreshing ? styles.centerEmpty : null}
/>
);
};
const styles = StyleSheet.create({
item: {
backgroundColor: '#e0f7fa',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 3,
},
title: {
fontSize: 16,
color: '#333',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
height: 200, // ある程度の高さを確保
},
centerEmpty: {
flexGrow: 1, // これにより、ListEmptyComponentが利用可能なスペースを占有し、中央に配置されやすくなる
justifyContent: 'center',
},
});
export default BasicRefreshExample;
解説
useCallback
:handleRefresh
関数をuseCallback
でラップすることで、コンポーネントが再レンダリングされるたびに不必要にこの関数が再作成されるのを防ぎます。これによりパフォーマンスが向上します。依存配列にdata
を含めているのは、fetchData
関数がdata
を参照して新しいデータを作成しているためです。handleRefresh
: この関数がonRefresh
に渡されます。setRefreshing(true)
: まずrefreshing
をtrue
に設定し、FlatList
が提供する標準のローディングインジケーターを表示させます。await fetchData()
: データを非同期に取得します。ここではダミー関数ですが、実際にはAPI呼び出しなどを行います。setData(newData)
: 取得した新しいデータでdata
ステートを更新し、リストを再レンダリングします。setRefreshing(false)
: データ取得が完了したらrefreshing
をfalse
に戻し、ローディングインジケーターを非表示にします。
useState(false)
:refreshing
ステートは、リストが現在更新中であるかどうかを示します。初期値はfalse
(更新中でない) です。
RefreshControl をカスタマイズする例
FlatList
は内部的に RefreshControl
コンポーネントを使用していますが、refreshControl
プロパティを使って明示的に RefreshControl
を渡すことで、より詳細なカスタマイズが可能です。
import React, { useState, useCallback, useEffect } from 'react';
import { FlatList, Text, View, StyleSheet, RefreshControl, Platform } from 'react-native';
const CustomRefreshControlExample = () => {
const [data, setData] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const fetchData = async () => {
return new Promise(resolve => {
setTimeout(() => {
const newItems = Array.from({ length: 5 }, (_, i) => ({
id: `${Date.now()}-${Math.random()}-${i}`,
title: `カスタマイズアイテム ${Date.now()}-${i}`
}));
resolve([...newItems, ...data]);
}, 1500);
});
};
useEffect(() => {
const loadInitialData = async () => {
setRefreshing(true);
const initialData = await new Promise(resolve => {
setTimeout(() => {
resolve(Array.from({ length: 10 }, (_, i) => ({
id: `${Date.now()}-initial-${i}`,
title: `初期カスタマイズアイテム ${i + 1}`
})));
}, 1000);
});
setData(initialData);
setRefreshing(false);
};
loadInitialData();
}, []);
const handleRefresh = useCallback(async () => {
setRefreshing(true);
const newData = await fetchData();
setData(newData);
setRefreshing(false);
}, [data]);
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
// refreshControl プロパティを使って RefreshControl コンポーネントを渡す
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
// iOS でのローディングインジケーターの色
tintColor="#007bff"
// Android でのローディングインジケーターの背景色
// backgroundColors は iOS では効果がありません
// colors は Android でのみ有効で、インジケーターの回転中の色を指定
{...(Platform.OS === 'android' ? {
progressBackgroundColor: '#f0f0f0', // Androidの背景色
colors: ['#007bff', '#28a745', '#dc3545'], // Androidのくるくるの色
} : {})}
title="更新中..." // プルダウン時に表示されるテキスト (iOSのみ)
titleColor="#007bff" // title のテキスト色 (iOSのみ)
/>
}
/>
);
};
const styles = StyleSheet.create({
item: {
backgroundColor: '#e0f7fa',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 3,
},
title: {
fontSize: 16,
color: '#333',
},
});
export default CustomRefreshControlExample;
解説
- プラットフォームごとの違い:
Platform.OS
を使って、Android と iOS で異なるプロパティを設定している点に注目してください。例えば、colors
やprogressBackgroundColor
は Android 特有のプロパティです。 refreshControl
プロパティ:FlatList
のrefreshing
とonRefresh
プロパティは、内部でRefreshControl
を使っています。直接RefreshControl
を渡すことで、tintColor
(iOS のインジケーター色)、colors
(Android のインジケーター色配列)、progressBackgroundColor
(Android のインジケーター背景色)、title
(iOS の更新中テキスト) などのプロパティを設定し、より細かく見た目をカスタマイズできます。
データが空の場合のプルツーリフレッシュ(ListEmptyComponent との連携)
データがまだ何もロードされていない、またはフィルターの結果データが空である場合でも、プルツーリフレッシュを可能にしたいことがあります。
import React, { useState, useCallback, useEffect } from 'react';
import { FlatList, Text, View, StyleSheet, ActivityIndicator, Button } from 'react-native';
const EmptyListRefreshExample = () => {
const [data, setData] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const fetchData = async () => {
return new Promise(resolve => {
setTimeout(() => {
// 新しいデータを生成 (最初は少し多め)
const newItems = Array.from({ length: Math.floor(Math.random() * 5) + 3 }, (_, i) => ({
id: `${Date.now()}-${Math.random()}-${i}`,
title: `新規アイテム ${Date.now()}-${i}`
}));
resolve(newItems);
}, 1500);
});
};
const loadInitialData = useCallback(async () => {
setRefreshing(true);
const initialData = await new Promise(resolve => {
setTimeout(() => {
// 最初は空のデータをシミュレートして、プルリフレッシュでデータをロードするようにする
resolve([]);
}, 500);
});
setData(initialData);
setRefreshing(false);
}, []);
useEffect(() => {
loadInitialData(); // 最初は空のリストから開始
}, [loadInitialData]);
const handleRefresh = useCallback(async () => {
setRefreshing(true);
const newData = await fetchData();
setData(newData); // 新しいデータで完全に置き換え
setRefreshing(false);
}, []);
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
refreshing={refreshing}
onRefresh={handleRefresh}
ListEmptyComponent={() => ( // データが空の場合に表示されるコンポーネント
<View style={styles.emptyContainer}>
{refreshing ? (
<ActivityIndicator size="large" color="#0000ff" />
) : (
<>
<Text style={styles.emptyText}>データがありません。</Text>
<Text style={styles.emptyText}>下に引っ張って更新してください。</Text>
<Button title="手動で更新" onPress={handleRefresh} />
</>
)}
</View>
)}
// データが空の場合でもスクロール可能にするために必要
contentContainerStyle={styles.fullHeight}
/>
);
};
const styles = StyleSheet.create({
item: {
backgroundColor: '#e0f7fa',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 3,
},
title: {
fontSize: 16,
color: '#333',
},
emptyContainer: {
flex: 1, // これにより、EmptyComponentが全高を占める
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
emptyText: {
fontSize: 16,
color: '#666',
marginBottom: 10,
},
fullHeight: {
flexGrow: 1, // これが重要。データが少なくてもスクロール可能にする
},
});
export default EmptyListRefreshExample;
handleRefresh
を手動で呼び出す:ListEmptyComponent
内にボタンを配置し、handleRefresh
を直接呼び出すことで、ユーザーがプルダウンしなくても更新を開始できるようにしています。contentContainerStyle={styles.fullHeight}
: これは非常に重要です。FlatList
は、data
が空の場合、その中身が空と見なされ、スクロール可能領域が計算されません。その結果、プルツーリフレッシュジェスチャーが認識されなくなります。flexGrow: 1
をcontentContainerStyle
に設定することで、FlatList
のコンテンツ領域が利用可能なスペース全体を占めるようになり、データが空でもプルツーリフレッシュが可能になります。ListEmptyComponent
:FlatList
のdata
プロパティが空の配列の場合に表示されるコンポーネントです。ここでは、データがないことを示すメッセージと、手動で更新をトリガーするためのボタン、そして更新中の場合はローディングインジケーターを表示しています。
onRefresh
はプルツーリフレッシュという特定のユーザー操作に対応するものですが、データの更新トリガーは他にもいくつか考えられます。
ボタンやアイコンによる手動更新
最も直接的な方法です。ユーザーが特定のボタンやアイコンをタップすることで、データの再取得をトリトリガーします。
使用例
- リスト内の更新ボタン
リストのフッターや、データがない場合に表示されるコンポーネント内に「更新」ボタンを配置する。 - ヘッダーの更新ボタン
画面上部のヘッダーに更新アイコン(例: リフレッシュマーク)を配置し、タップ時にデータを再取得する。
コード例
import React, { useState, useCallback, useEffect } from 'react';
import { FlatList, Text, View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons'; // 例: react-native-vector-icons を使用
const ButtonRefreshExample = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false); // ローディング状態
const [error, setError] = useState(null); // エラー状態
const fetchData = async () => {
return new Promise(resolve => {
setTimeout(() => {
const newItems = Array.from({ length: 10 }, (_, i) => ({
id: `${Date.now()}-${Math.random()}-${i}`,
title: `アイテム ${Date.now()}-${i}`
}));
resolve(newItems);
}, 1500);
});
};
const handleRefresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const newData = await fetchData();
setData(newData);
} catch (err) {
setError("データの取得に失敗しました。");
console.error(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
handleRefresh(); // 初回ロード
}, [handleRefresh]);
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>データリスト</Text>
<TouchableOpacity onPress={handleRefresh} disabled={loading}>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Icon name="refresh" size={24} color="#fff" />
)}
</TouchableOpacity>
</View>
{error && <Text style={styles.errorText}>{error}</Text>}
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
{loading ? (
<ActivityIndicator size="large" color="#0000ff" />
) : (
<>
<Text style={styles.emptyText}>データがありません。</Text>
<TouchableOpacity onPress={handleRefresh}>
<Text style={styles.retryButton}>再試行</Text>
</TouchableOpacity>
</>
)}
</View>
)}
contentContainerStyle={data.length === 0 && !loading ? styles.centerEmpty : null}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f8f8',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 15,
backgroundColor: '#6200EE',
paddingTop: 50, // ステータスバー対策
},
headerTitle: {
color: '#fff',
fontSize: 20,
fontWeight: 'bold',
},
item: {
backgroundColor: '#fff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
title: {
fontSize: 16,
color: '#333',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
minHeight: 200, // データがなくてもスクロール可能にするために
},
emptyText: {
fontSize: 16,
color: '#666',
marginBottom: 10,
},
retryButton: {
color: '#6200EE',
fontSize: 16,
fontWeight: 'bold',
marginTop: 10,
},
centerEmpty: {
flexGrow: 1,
justifyContent: 'center',
},
errorText: {
color: 'red',
textAlign: 'center',
padding: 10,
}
});
export default ButtonRefreshExample;
利点
- ネットワークエラー時などに再試行を促しやすい。
- ユーザーに明確な更新の機会を提供する。
欠点
- ユーザーが明示的に操作する必要がある。
アプリの再開時やタブ切り替え時の自動更新
アプリがフォアグラウンドに戻ってきたときや、特定のタブ(例えば、ニュースフィードのタブ)に切り替わったときに、自動的にデータを更新する。
使用例
- @react-navigation/native の useFocusEffect
ナビゲーションスタックでスクリーンがフォーカスされたときに実行する。 - AppState API
アプリがバックグラウンドからフォアグラウンドに移行したことを検知する。
コード例 (@react-navigation/native の例)
import React, { useState, useCallback, useEffect } from 'react';
import { FlatList, Text, View, StyleSheet, ActivityIndicator } from 'react-native';
import { useFocusEffect } from '@react-navigation/native'; // navigationを使用している場合
const AutoRefreshOnFocusExample = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const fetchData = async () => {
return new Promise(resolve => {
setTimeout(() => {
console.log('Fetching new data...');
const newItems = Array.from({ length: 7 }, (_, i) => ({
id: `${Date.now()}-${Math.random()}-${i}`,
title: `自動更新アイテム ${Date.now()}-${i}`
}));
resolve(newItems);
}, 1000);
});
};
const loadData = useCallback(async () => {
setLoading(true);
const newData = await fetchData();
setData(newData);
setLoading(false);
}, []);
// スクリーンがフォーカスされるたびにデータをロード
useFocusEffect(
useCallback(() => {
loadData();
return () => {
// スクリーンがフォーカスを失ったときのクリーンアップ処理(必要であれば)
console.log('Screen unfocused. Cleaning up...');
};
}, [loadData])
);
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.headerText}>スクリーンフォーカス時に自動更新</Text>
{loading && data.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff" />
<Text>ロード中...</Text>
</View>
) : (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>データがありません。</Text>
</View>
)}
contentContainerStyle={data.length === 0 ? styles.centerEmpty : null}
/>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
backgroundColor: '#f0f4f8',
},
headerText: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
color: '#333',
},
item: {
backgroundColor: '#fff',
padding: 15,
marginVertical: 6,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
title: {
fontSize: 16,
color: '#333',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: 200, // 適切な高さ
},
emptyText: {
fontSize: 16,
color: '#666',
},
centerEmpty: {
flexGrow: 1,
justifyContent: 'center',
},
});
export default AutoRefreshOnFocusExample;
利点
- 特にタブベースのアプリで、ユーザーがタブを切り替えるたびに新鮮な体験を提供できる。
- ユーザーが意識せずに常に最新のデータを見ることができる。
欠点
- 不必要な更新を避けるためのロジック(例: 前回の更新からN分経過しているか確認するなど)が必要になる場合がある。
- 頻繁な更新はネットワーク負荷やバッテリー消費につながる可能性がある。
定期的な自動更新(ポーリング)
特定の時間間隔で自動的にデータを取得する。リアルタイム性が求められるが、WebSocketなどを使うほどではない場合に有効です。
使用例
- ニュースのティッカーや株価情報など、定期的に情報が更新されるもの。
コード例
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { FlatList, Text, View, StyleSheet, ActivityIndicator } from 'react-native';
const PollingRefreshExample = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const intervalRef = useRef(null); // setInterval ID を保持
const fetchData = async () => {
console.log('Polling for new data...');
return new Promise(resolve => {
setTimeout(() => {
const newItems = Array.from({ length: 3 }, (_, i) => ({
id: `${Date.now()}-${Math.random()}-${i}`,
title: `ポーリング更新 ${Date.now()}-${i}`
}));
// 既存のデータの先頭に新しいアイテムを追加
resolve([...newItems, ...data]);
}, 1000);
});
};
const startPolling = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = setInterval(async () => {
setLoading(true);
const newData = await fetchData();
setData(newData);
setLoading(false);
}, 5000); // 5秒ごとに更新
}, [data]); // dataが変更されたらポーリング関数も再作成される可能性がある
const stopPolling = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
console.log('Polling stopped.');
}
}, []);
useEffect(() => {
// 初回ロード
setLoading(true);
fetchData().then(initialData => {
setData(initialData);
setLoading(false);
startPolling(); // 初回ロード後にポーリングを開始
});
// コンポーネントがアンマウントされたときにポーリングを停止
return () => {
stopPolling();
};
}, [startPolling, stopPolling]); // 依存配列に含める
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.headerText}>5秒ごとに自動更新中...</Text>
{loading && data.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff" />
<Text>初回ロード中...</Text>
</View>
) : (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>データがありません。</Text>
</View>
)}
contentContainerStyle={data.length === 0 ? styles.centerEmpty : null}
/>
)}
{loading && data.length > 0 && (
<ActivityIndicator size="small" color="#0000ff" style={styles.bottomLoading} />
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
backgroundColor: '#e3f2fd',
},
headerText: {
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 15,
color: '#007bff',
},
item: {
backgroundColor: '#fff',
padding: 15,
marginVertical: 6,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
title: {
fontSize: 16,
color: '#333',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: 200,
},
emptyText: {
fontSize: 16,
color: '#666',
},
centerEmpty: {
flexGrow: 1,
justifyContent: 'center',
},
bottomLoading: {
padding: 10,
}
});
export default PollingRefreshExample;
利点
- ユーザーの操作を必要としない。
- リアルタイムに近いデータ表示が可能。
欠点
- よりリアルタイム性が求められる場合は、WebSocket などのプッシュ通知技術が適している。
- バッテリー消費が増加する可能性がある。
- ネットワークとサーバーへの負荷が高い。
プッシュ通知による更新
サーバーから新しいデータが利用可能になったことをクライアントに通知し、それを受けてデータを更新する。
使用例
- ソーシャルメディアの通知
- チャットアプリの新着メッセージ
技術
- Firebase Cloud Messaging (FCM) / Apple Push Notification service (APNs)
アプリがフォアグラウンドでなくても、サーバーからデバイスに通知を送信し、アプリが起動した際やユーザーが通知をタップした際にデータを更新する。 - WebSocket
クライアントとサーバー間で双方向の通信チャネルを確立し、リアルタイムのデータ更新を可能にする。
コード例 (概念のみ - 実際のプッシュ通知実装は複雑)
// これは概念コードであり、完全な動作例ではありません。
// 実際のプッシュ通知の実装には、Firebaseなどのサービス設定とネイティブコードの記述が必要になります。
import React, { useState, useEffect, useCallback } from 'react';
import { FlatList, Text, View, StyleSheet, Alert } from 'react-native';
// import messaging from '@react-native-firebase/messaging'; // Firebaseの場合
const PushNotificationRefreshExample = () => {
const [data, setData] = useState([]);
// ダミーのデータ取得関数
const fetchData = async (origin = 'initial') => {
return new Promise(resolve => {
setTimeout(() => {
const newItems = Array.from({ length: 1 }, (_, i) => ({
id: `${Date.now()}-${Math.random()}-${i}`,
title: `${origin}経由の新しいアイテム ${Date.now()}`
}));
resolve([...newItems, ...data]);
}, 500);
});
};
useEffect(() => {
fetchData('初回ロード').then(initialData => setData(initialData));
// ここにプッシュ通知のリスナーを設定する
// 例: Firebase Cloud Messaging の場合
// const unsubscribe = messaging().onMessage(async remoteMessage => {
// Alert.alert('新しいデータが利用可能です!', remoteMessage.notification.body);
// // 通知を受け取ったらデータを更新
// const updatedData = await fetchData('プッシュ通知');
// setData(updatedData);
// });
// return unsubscribe; // クリーンアップ関数
}, [data]); // dataに依存している場合は、依存配列に含める
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.headerText}>プッシュ通知による更新(概念)</Text>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
backgroundColor: '#fff',
},
headerText: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
item: {
backgroundColor: '#f0f0f0',
padding: 15,
marginVertical: 5,
marginHorizontal: 10,
borderRadius: 5,
},
title: {
fontSize: 16,
},
});
export default PushNotificationRefreshExample;
利点
- バッテリー消費が少ない。
- ネットワーク負荷が低い(必要な時だけ通信する)。
- 最もリアルタイム性の高い更新方法。
- プッシュ通知の場合は、ユーザーが通知を許可する必要がある。
- WebSocket の場合は、常に接続を維持する必要がある(バッテリー消費に影響)。
- サーバー側の設定と実装が複雑。