Skip to content

VirtualizedLists should never be nested inside plain ScrollViews

問題の概要

React Nativeアプリケーション開発において、FlatListコンポーネントをScrollView内に配置しようとすると、次のエラーメッセージが表示されることがあります:

VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing and other functionality - use another VirtualizedList-backed container instead.

この問題は、同じ方向(通常は垂直方向)のスクロール可能なコンポーネントがネストされている場合に発生します。FlatListは内部でVirtualizedListを使用しており、これはスクロール可能なコンテナです。これを同じ方向のScrollView内に配置すると、スクロール動作の競合やパフォーマンスの問題が発生します。

基本的な解決策

解決策1: scrollEnabledプロパティの無効化

最も一般的で簡単な解決法は、FlatListのスクロールを無効にすることです:

tsx
<ScrollView contentContainerStyle={style}>
   {/* 他のコンポーネント */}
   <FlatList
       style={style}
       data={data}
       scrollEnabled={false}  // これを追加
       keyExtractor={(item, index) => index.toString()}
       renderItem={({ item, index}) => (/* レンダリング処理 */)}
   />
   {/* 他のコンポーネント */}
</ScrollView>

これにより、親のScrollViewのみがスクロールを制御し、競合が解消されます。

TIP

scrollEnabled={false}に加えて、nestedScrollEnabled={true}を設定すると、よりスムーズなスクロール動作が得られる場合があります。

解決策2: 単純なmap処理への置き換え

リストが比較的短い場合(数十項目以下)、FlatListの代わりに単純なmap処理を使用できます:

tsx
<ScrollView contentContainerStyle={style}>
   {/* 他のコンポーネント */}
   {data.map((item, index) => (
     <View key={index.toString()}>
       {/* アイテムのレンダリング処理 */}
     </View>
   ))}
   {/* 他のコンポーネント */}
</ScrollView>

WARNING

この方法はリストが長い場合(100項目以上)にはパフォーマンス問題を引き起こす可能性があるため、注意が必要です。

高度な解決策

解決策3: FlatListのヘッダー/フッター機能の活用

FlatListListHeaderComponentListFooterComponentプロパティを使用して、ScrollViewを完全に排除する方法です:

tsx
<FlatList
    style={style}
    data={data}
    keyExtractor={(item, index) => index.toString()}
    renderItem={({ item, index}) => (/* レンダリング処理 */}
    ListHeaderComponent={() => (
        // ScrollViewの上部にあったコンポーネント
    )}
    ListFooterComponent={() => (
        // ScrollViewの下部にあったコンポーネント
    )}
/>

この方法では、単一のFlatListですべてのコンテンツを扱うため、パフォーマンス最適化のメリットを維持できます。

解決策4: SectionListの使用

複数のセクションを持つコンテンツの場合、SectionListの使用が適しています:

tsx
<SectionList
    sections={[
        {
            title: 'ヘッダーセクション',
            data: [],
            renderItem: () => (
                // ヘッダーコンポーネント
            )
        },
        {
            title: 'メインリスト',
            data: data,
            renderItem: ({ item, index }) => (/* レンダリング処理 */)
        },
        {
            title: 'フッターセクション',
            data: [],
            renderItem: () => (
                // フッターコンポーネント
            )
        }
    ]}
    keyExtractor={(item, index) => index.toString()}
/>

特殊なケースの対応

異なる方向のスクロールが必要な場合

親のScrollViewが垂直方向で、子のFlatListが水平方向の場合は、競合しないため問題ありません:

tsx
<ScrollView> {/* 垂直方向 */}
  <Text>ヘッダー</Text>
  <FlatList
     horizontal={true}  // 水平方向
     data={data}
     renderItem={({item}) => <Text>{item}</Text>}
  />
</ScrollView>

カスタムVirtualizedScrollViewの使用

専用のライブラリを使用する方法もあります:

tsx
import { ScrollView } from 'react-native-virtualized-view'

<ScrollView>
   {/* 他のコンポーネント */}
   <FlatList
       data={data}
       scrollEnabled={false}
       renderItem={({ item }) => <ItemComponent item={item} />}
   />
</ScrollView>

または、カスタムコンポーネントを作成することもできます:

tsx
const VirtualizedScrollView = props => {
  return (
    <FlatList
      {...props}
      data={[]}
      keyExtractor={(e, i) => 'dom' + i.toString()}
      ListEmptyComponent={null}
      renderItem={null}
      ListHeaderComponent={() => (
        <>{props.children}</>
      )}
    />
  );
};

パフォーマンス考慮事項

FlatListは仮想化リストであり、表示されている要素のみをレンダリングするため、長いリストではパフォーマンス上の大きな利点があります。単純なmap処理に置き換える場合は、以下の点を考慮してください:

  • リスト項目が少ない(〜50件):map処理で問題ない
  • リスト項目が多い(50件以上):FlatListscrollEnabled={false}で使用する
  • 非常に長いリスト(100件以上):FlatListの仮想化機能が必須

推奨アプローチ

一般的には、以下の判断フローが推奨されます:

まとめ

VirtualizedLists should never be nested inside plain ScrollViewsエラーは、同じ方向のスクロール可能なコンポーネントがネストされている場合に発生します。最も簡単で効果的な解決法は、FlatListscrollEnabledプロパティをfalseに設定することです。リストの長さや要件に応じて、map処理への置き換えやSectionListの使用など、適切な解決法を選択してください。

パフォーマンスとユーザーエクスペリエンスを最適化するために、常にコンテンツの性質に最も適したアプローチを選択することが重要です。