【トラブルシューティング】React Native FlatList horizontalでよくあるエラーと解決策

2025-05-31

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;

上記の例では、FlatListhorizontal プロパティが 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
    FlatListcontentContainerStyle プロパティを使用して、コンテンツ全体の幅を明示的に設定するか、子アイテムの幅の合計が FlatList の幅よりも大きくなるように調整してください。

    <FlatList
      // ... 他のプロパティ
      contentContainerStyle={{ width: data.length * 220 }} // アイテム数 * (アイテム幅 + marginHorizontal * 2) より少し大きめに設定
      horizontal={true}
    />
    
  • 原因2
    FlatList 自体の幅が、すべての子アイテムを横に並べても収まる程度の幅しかない場合、スクロールする必要がなくなり、スクロールバーも表示されません。

  • 解決策1
    親コンポーネントのスタイルを確認し、overflow: hidden が設定されている場合は削除するか、overflowX: auto または overflowX: scroll を設定してみてください。

  • 原因1
    FlatList を囲んでいる親コンポーネントのスタイルによって、横方向のスクロールが制限されている可能性があります。例えば、親コンポーネントに overflow: hidden が設定されている場合などです。

最初のアイテムや最後のアイテムが途中で切れて表示される

  • 解決策

    • FlatListcontentContainerStylepaddingHorizontal を追加して、両端に余白を設ける。
    • 各アイテムの marginHorizontal を調整する。
    • 必要であれば、最初のアイテムと最後のアイテムに特別なスタイルを適用する。
    <FlatList
      // ... 他のプロパティ
      contentContainerStyle={{ paddingHorizontal: 16 }}
      horizontal={true}
    />
    
  • 原因
    FlatList 自体やアイテムの marginHorizontal などのスタイル設定により、最初のアイテムの左端や最後のアイテムの右端が見切れてしまうことがあります。

横スクロールのパフォーマンスが悪い / カクつく

  • 解決策
    • React.memo を使用して、props が変更されない限りコンポーネントの再レンダリングを防ぐ。
    • 画像をリサイズしたり、WebP 形式などの効率的な形式を使用する。
    • shouldComponentUpdate ライフサイクルメソッド(クラスコンポーネントの場合)や useCallbackuseMemo などのフックを使用して、不要な関数やオブジェクトの再生成を防ぐ。
    • 必要に応じて、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}
      />
    );
    
  • 原因
    FlatListref を使用して特定のメソッド(例: 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;

この例では、FlatListhorizontal プロパティに 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 で囲み、その ViewScrollView で水平にスクロールさせる方法です。これは、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 の使用とパフォーマンス上の違いはほとんどありません。

カスタム実装

より複雑なレイアウトやインタラクションが必要な場合は、ScrollViewonScroll イベントなどを利用して、アイテムの表示状態を自身で管理するカスタム実装を行うことも考えられます。しかし、これは高度なテクニックであり、FlatList が提供する最適化を自身で実装する必要があるため、一般的には推奨されません。

FlatList#horizontal を使用することの利点

FlatList は、大量のデータを効率的に表示するために最適化されています。主な利点は以下の通りです。

  • 性能最適化
    getItemLayout などのプロパティを利用することで、スクロール位置の計算を最適化できます。
  • アイテムの再利用 (Recycling)
    スクロールアウトしたアイテムのビューを再利用して、新しいアイテムを表示するため、パフォーマンスが向上します。
  • 遅延レンダリング (Lazy Loading)
    画面に表示されるアイテムのみをレンダリングするため、初期ロード時間が短縮され、メモリ使用量が削減されます。

FlatList#horizontal は、水平スクロールするリストを効率的に実装するための推奨される方法です。ScrollView は、データ量が少なく、動的な変更が少ない場合にシンプルな代替案となります。カスタム実装は、非常に特殊な要件がある場合に検討されるべきですが、複雑さとメンテナンスのコストが高くなります。