【トラブルシューティング】React Native FlatList horizontalでよくあるエラーと解決策
FlatList
コンポーネントの horizontal
プロパティは、リストのアイテムを垂直方向(縦方向)に並べるのではなく、水平方向(横方向)に並べて表示するかどうかを指定するために使用します。
具体的には、horizontal
プロパティに true
を設定すると、FlatList
内の各アイテムが左から右へ、または右から左へ(レイアウトの方向によります)と、横一列に配置されます。デフォルトでは false
に設定されており、アイテムは縦に並びます。
horizontal を true に設定する主な用途
- 限られたスペースでの表示
画面の高さが限られている場合に、横スクロールを利用して多くの情報を表示できます。 - 横スクロールするリスト
例えば、画像ギャラリー、おすすめの商品リスト、日付の選択肢などを横にスクロールして表示したい場合に便利です。
使用例
import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
const App = () => {
const data = [
{ id: '1', title: 'アイテム 1' },
{ id: '2', title: 'アイテム 2' },
{ id: '3', title: 'アイテム 3' },
{ id: '4', title: 'アイテム 4' },
{ id: '5', title: 'アイテム 5' },
];
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
);
return (
<View style={styles.container}>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
horizontal={true} // ここで horizontal を true に設定
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 22,
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
},
title: {
fontSize: 24,
},
});
export default App;
上記の例では、FlatList
の horizontal
プロパティが true
に設定されているため、「アイテム 1」から「アイテム 5」までの各アイテムが横一列に並んで表示され、横方向にスクロールできるようになります。
- 横スクロールのインジケーター(スクロールバー)の表示・非表示を制御したい場合は、
showsHorizontalScrollIndicator
プロパティを使用します。 horizontal={true}
を使用する場合、各アイテムの幅を適切に設定しないと、アイテムが重なって表示されたり、意図しない表示になることがあります。renderItem
内のスタイルで各アイテムの幅を指定するようにしてください。
アイテムが横一列に並ばない / 重なって表示される
-
解決策
renderItem
関数内でレンダリングするコンポーネントにStyleSheet
を適用し、明確なwidth
を設定してください。const renderItem = ({ item }) => ( <View style={[styles.item, { width: 200 }]}> {/* 幅を 200 に設定 */} <Text style={styles.title}>{item.title}</Text> </View> ); const styles = StyleSheet.create({ item: { backgroundColor: '#f9c2ff', padding: 20, marginVertical: 8, marginHorizontal: 16, }, // ... 他のスタイル });
-
原因
renderItem
でレンダリングされる各アイテムのwidth
スタイルが適切に設定されていない可能性があります。horizontal={true}
に設定した場合、各アイテムの幅を指定しないと、デフォルトの幅が適用され、アイテムが横に並びきらずに重なって表示されることがあります。
横スクロールができない
-
解決策2
FlatList
のcontentContainerStyle
プロパティを使用して、コンテンツ全体の幅を明示的に設定するか、子アイテムの幅の合計がFlatList
の幅よりも大きくなるように調整してください。<FlatList // ... 他のプロパティ contentContainerStyle={{ width: data.length * 220 }} // アイテム数 * (アイテム幅 + marginHorizontal * 2) より少し大きめに設定 horizontal={true} />
-
原因2
FlatList
自体の幅が、すべての子アイテムを横に並べても収まる程度の幅しかない場合、スクロールする必要がなくなり、スクロールバーも表示されません。 -
解決策1
親コンポーネントのスタイルを確認し、overflow: hidden
が設定されている場合は削除するか、overflowX: auto
またはoverflowX: scroll
を設定してみてください。 -
原因1
FlatList
を囲んでいる親コンポーネントのスタイルによって、横方向のスクロールが制限されている可能性があります。例えば、親コンポーネントにoverflow: hidden
が設定されている場合などです。
最初のアイテムや最後のアイテムが途中で切れて表示される
-
解決策
FlatList
のcontentContainerStyle
にpaddingHorizontal
を追加して、両端に余白を設ける。- 各アイテムの
marginHorizontal
を調整する。 - 必要であれば、最初のアイテムと最後のアイテムに特別なスタイルを適用する。
<FlatList // ... 他のプロパティ contentContainerStyle={{ paddingHorizontal: 16 }} horizontal={true} />
-
原因
FlatList
自体やアイテムのmarginHorizontal
などのスタイル設定により、最初のアイテムの左端や最後のアイテムの右端が見切れてしまうことがあります。
横スクロールのパフォーマンスが悪い / カクつく
- 解決策
React.memo
を使用して、props が変更されない限りコンポーネントの再レンダリングを防ぐ。- 画像をリサイズしたり、WebP 形式などの効率的な形式を使用する。
shouldComponentUpdate
ライフサイクルメソッド(クラスコンポーネントの場合)やuseCallback
、useMemo
などのフックを使用して、不要な関数やオブジェクトの再生成を防ぐ。- 必要に応じて、
getItemLayout
プロパティを実装して、スクロール位置の計算を最適化する。
- 原因
- レンダリングするアイテムのコンポーネントが複雑すぎる。
- 画像などの大きなアセットを最適化せずに使用している。
- 不要な再レンダリングが発生している。
keyExtractor の設定ミス
-
解決策
keyExtractor
には、リスト内の各アイテムを一意に識別できる文字列を返す関数を設定してください。通常は、アイテムオブジェクトのid
プロパティなどを使用します。<FlatList // ... 他のプロパティ keyExtractor={item => item.id.toString()} // id が数値の場合でも toString() で文字列に変換 horizontal={true} />
-
原因
keyExtractor
プロパティが正しく設定されていない場合、アイテムの追加、削除、並び替えなどが正しく行われず、パフォーマンスにも影響を与える可能性があります。
ref の取り扱いに関するエラー
-
解決策
useRef
フックを使用してref
を作成し、useEffect
フック内でref.current
が存在することを確認してからメソッドを呼び出すようにします。const flatListRef = useRef(null); useEffect(() => { if (flatListRef.current) { // flatListRef.current を使用した処理 flatListRef.current.scrollToOffset({ offset: 0, animated: true }); } }, []); return ( <FlatList ref={flatListRef} // ... 他のプロパティ horizontal={true} /> );
-
原因
FlatList
のref
を使用して特定のメソッド(例:scrollToOffset
) を呼び出す際に、タイミングによってはref
がまだコンポーネントにアタッチされていないことがあります。
基本的な横スクロールリストの例
これは、最も基本的な横スクロールするリストの例です。複数のアイテムが横一列に表示され、左右にスクロールできます。
import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
const data = [
{ id: '1', title: 'アイテム 1' },
{ id: '2', title: 'アイテム 2' },
{ id: '3', title: 'アイテム 3' },
{ id: '4', title: 'アイテム 4' },
{ id: '5', title: 'アイテム 5' },
{ id: '6', title: 'アイテム 6' },
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const App = () => {
const renderItem = ({ item }) => (
<Item title={item.title} />
);
return (
<View style={styles.container}>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
horizontal={true} // 水平スクロールを有効にする
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 22,
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
width: 200, // 各アイテムの幅を明示的に設定
},
title: {
fontSize: 24,
},
});
export default App;
この例では、FlatList
の horizontal
プロパティに true
を設定することで、リストが水平方向にレンダリングされます。重要なのは、各アイテムのスタイル (styles.item
) に width
を設定している点です。これがないと、アイテムが重なって表示される可能性があります。
横スクロールリストとセクションヘッダーの組み合わせ例
SectionList
コンポーネントと組み合わせることで、横スクロールするセクション付きリストを作成することもできます。
import React from 'react';
import { SectionList, StyleSheet, Text, View } from 'react-native';
const sections = [
{
title: 'セクション 1',
data: [{ id: '1', title: 'アイテム 1-1' }, { id: '2', title: 'アイテム 1-2' }],
},
{
title: 'セクション 2',
data: [{ id: '3', title: 'アイテム 2-1' }, { id: '4', title: 'アイテム 2-2' }, { id: '5', title: 'アイテム 2-3' }],
},
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const Header = ({ title }) => (
<View style={styles.header}>
<Text style={styles.headerText}>{title}</Text>
</View>
);
const App = () => {
return (
<View style={styles.container}>
<SectionList
sections={sections}
keyExtractor={(item, index) => item.id + index}
renderItem={({ item }) => <Item title={item.title} />}
renderSectionHeader={({ section: { title } }) => <Header title={title} />}
horizontal={true} // SectionList も horizontal に設定
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 22,
},
header: {
backgroundColor: '#e0e0e0',
padding: 10,
marginVertical: 8,
},
headerText: {
fontSize: 18,
fontWeight: 'bold',
},
item: {
backgroundColor: '#cceeff',
padding: 20,
marginHorizontal: 8,
width: 150,
},
title: {
fontSize: 16,
},
});
export default App;
この例では、SectionList
自体に horizontal={true}
を設定することで、セクションヘッダーとアイテムが横方向に並びます。各アイテムの幅も styles.item
で指定しています。
横スクロールリストで中央のアイテムを強調する例
スクロールに合わせて中央のアイテムを少し大きく表示したり、スタイルを変更したりするテクニックです。onScroll
イベントと Animated
API を組み合わせて実現できます。
import React, { useRef, useState, useEffect } from 'react';
import { FlatList, StyleSheet, Text, View, Animated, Dimensions } from 'react-native';
const { width: screenWidth } = Dimensions.get('window');
const ITEM_WIDTH = 200;
const ITEM_MARGIN = 16;
const TOTAL_ITEM_WIDTH = ITEM_WIDTH + ITEM_MARGIN * 2;
const data = Array.from({ length: 20 }, (_, index) => ({ id: String(index), title: `アイテム ${index + 1}` }));
const App = () => {
const scrollX = useRef(new Animated.Value(0)).current;
const [currentIndex, setCurrentIndex] = useState(0);
const flatListRef = useRef(null);
useEffect(() => {
const listenerId = scrollX.addListener(({ value }) => {
const index = Math.round(value / TOTAL_ITEM_WIDTH);
setCurrentIndex(index);
});
return () => scrollX.removeListener(listenerId);
}, [scrollX]);
const renderItem = ({ item, index }) => {
const inputRange = [(index - 1) * TOTAL_ITEM_WIDTH, index * TOTAL_ITEM_WIDTH, (index + 1) * TOTAL_ITEM_WIDTH];
const scale = scrollX.interpolate({
inputRange,
outputRange: [0.8, 1, 0.8],
extrapolate: 'clamp',
});
return (
<Animated.View style={[styles.item, { width: ITEM_WIDTH, transform: [{ scale }] }]}>
<Text style={styles.title}>{item.title}</Text>
</Animated.View>
);
};
const scrollToItem = (index) => {
flatListRef.current?.scrollToOffset({
offset: index * TOTAL_ITEM_WIDTH,
animated: true,
});
};
return (
<View style={styles.container}>
<Animated.FlatList
ref={flatListRef}
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
horizontal={true}
showsHorizontalScrollIndicator={false}
snapToInterval={TOTAL_ITEM_WIDTH}
decelerationRate="fast"
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
contentContainerStyle={{ paddingHorizontal: (screenWidth - ITEM_WIDTH) / 2 }}
/>
<View style={styles.indicatorContainer}>
{data.map((_, index) => (
<View
key={index}
style={[styles.indicator, currentIndex === index && styles.indicatorActive]}
onTouchStart={() => scrollToItem(index)}
/>
))}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 50,
alignItems: 'center',
},
item: {
backgroundColor: '#90caf9',
padding: 20,
marginHorizontal: ITEM_MARGIN,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 18,
fontWeight: 'bold',
color: 'white',
},
indicatorContainer: {
flexDirection: 'row',
marginTop: 20,
},
indicator: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: '#ccc',
marginHorizontal: 5,
},
indicatorActive: {
backgroundColor: '#007bff',
},
});
export default App;
ScrollView を使用する
最も基本的な代替案の一つは、ScrollView
コンポーネントを使用することです。ScrollView
は、その子要素がコンテンツ領域よりも大きい場合にスクロール可能なビューを提供します。horizontal
プロパティを true
に設定することで、水平スクロールを実現できます。
import React from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
const data = [
{ id: '1', title: 'アイテム 1' },
{ id: '2', title: 'アイテム 2' },
{ id: '3', title: 'アイテム 3' },
{ id: '4', title: 'アイテム 4' },
{ id: '5', title: 'アイテム 5' },
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const App = () => {
return (
<ScrollView horizontal={true} style={styles.container}>
{data.map(item => (
<Item key={item.id} title={item.title} />
))}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row', // 子要素を横に並べる
paddingVertical: 20,
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginHorizontal: 16,
width: 200,
},
title: {
fontSize: 24,
},
});
export default App;
利点
- 動的なアイテムの追加や削除が少ない、固定されたコンテンツの水平スクロールに適している。
- シンプルで理解しやすい。
欠点
- 遅延レンダリングやアイテムの再利用の仕組みがないため、メモリ使用量が増加する可能性がある。
- 大量のデータを扱う場合、すべてのアイテムを一度にレンダリングするため、パフォーマンスが低下する可能性がある。
View と ScrollView を組み合わせて使用する
複数のアイテムを View
で囲み、その View
を ScrollView
で水平にスクロールさせる方法です。これは、ScrollView
の直接の子要素として複数のアイテムを配置するのと同様の結果になります。
import React from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
const data = [
{ id: '1', title: 'アイテム 1' },
{ id: '2', title: 'アイテム 2' },
{ id: '3', title: 'アイテム 3' },
{ id: '4', title: 'アイテム 4' },
{ id: '5', title: 'アイテム 5' },
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const App = () => {
return (
<ScrollView horizontal={true} style={styles.container}>
<View style={styles.innerContainer}>
{data.map(item => (
<Item key={item.id} title={item.title} />
))}
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
paddingVertical: 20,
},
innerContainer: {
flexDirection: 'row', // 子要素を横に並べる
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginHorizontal: 16,
width: 200,
},
title: {
fontSize: 24,
},
});
export default App;
この方法は、構造を少し明示的にする場合に使われることがありますが、基本的な ScrollView
の使用とパフォーマンス上の違いはほとんどありません。
カスタム実装
より複雑なレイアウトやインタラクションが必要な場合は、ScrollView
の onScroll
イベントなどを利用して、アイテムの表示状態を自身で管理するカスタム実装を行うことも考えられます。しかし、これは高度なテクニックであり、FlatList
が提供する最適化を自身で実装する必要があるため、一般的には推奨されません。
FlatList#horizontal
を使用することの利点
FlatList
は、大量のデータを効率的に表示するために最適化されています。主な利点は以下の通りです。
- 性能最適化
getItemLayout
などのプロパティを利用することで、スクロール位置の計算を最適化できます。 - アイテムの再利用 (Recycling)
スクロールアウトしたアイテムのビューを再利用して、新しいアイテムを表示するため、パフォーマンスが向上します。 - 遅延レンダリング (Lazy Loading)
画面に表示されるアイテムのみをレンダリングするため、初期ロード時間が短縮され、メモリ使用量が削減されます。
FlatList#horizontal
は、水平スクロールするリストを効率的に実装するための推奨される方法です。ScrollView
は、データ量が少なく、動的な変更が少ない場合にシンプルな代替案となります。カスタム実装は、非常に特殊な要件がある場合に検討されるべきですが、複雑さとメンテナンスのコストが高くなります。