【初心者向け】React Native FlatList スムーズなスクロール実装ガイド (scrollToIndex)

2025-05-31

基本的な使い方

<FlatList>ref を取得し、その ref を通して scrollToIndex() メソッドを呼び出します。

import React, { useRef } from 'react';
import { FlatList, View, Text, StyleSheet } from 'react-native';

const MyList = () => {
  const flatListRef = useRef(null);
  const data = Array.from({ length: 50 }, (_, index) => ({ key: String(index), value: `Item ${index}` }));

  const scrollToItem = (index) => {
    if (flatListRef.current) {
      flatListRef.current.scrollToIndex({ animated: true, index: index });
    }
  };

  return (
    <View>
      <FlatList
        ref={flatListRef}
        data={data}
        renderItem={({ item }) => <Text style={styles.item}>{item.value}</Text>}
      />
      {/* ボタンなどで scrollToItem を呼び出す */}
      {/* 例: <Button title="Go to Item 20" onPress={() => scrollToItem(20)} /> */}
    </View>
  );
};

const styles = StyleSheet.create({
  item: {
    padding: 10,
    fontSize: 18,
    height: 44,
  },
});

export default MyList;

scrollToIndex() メソッドの引数

scrollToIndex() メソッドは、以下のプロパティを持つオブジェクトを引数として受け取ります。

  • viewPosition (任意)
    スクロール後のアイテムをビューポートのどの位置に配置するかを指定する数値です。以下の値が使用できます。
    • 0: アイテムの先頭をビューポートの先頭に合わせます(デフォルト)。
    • 0.5: アイテムの中央をビューポートの中央に合わせます。
    • 1: アイテムの末尾をビューポートの末尾に合わせます。
  • viewOffset (任意)
    スクロール後のアイテムの先頭が、<FlatList> のビューポートの先頭からどれだけオフセットされるかを指定する数値です。デフォルトは 0 です。例えば、ヘッダーなどが存在し、アイテムがヘッダーの下に表示されるようにしたい場合に便利です。
  • index (必須)
    スクロール先のアイテムのインデックス(0から始まる数値)です。
  • animated (任意)
    スクロールをアニメーションさせるかどうかを指定するブール値です。デフォルトは true です。false に設定すると、瞬時に指定されたアイテムまでスクロールします。
  • 指定した indexdata 配列の範囲外である場合、予期しない動作をする可能性があります。インデックスの範囲を適切に管理してください。
  • scrollToIndex() は、<FlatList> がレンダリングされ、ref が正しく設定された後に呼び出す必要があります。コンポーネントのマウント時など、適切なタイミングで呼び出すようにしてください。
  • scrollToIndex() を使用するには、<FlatList>ref を設定する必要があります。useRef() フックを使って ref を作成し、<FlatList>ref プロパティに渡します。


よくあるエラーとトラブルシューティング

    • エラー
      指定した indexdata 配列の範囲外である場合、スクロールが正常に行われないか、予期しない動作をする可能性があります。
    • 原因
      index に誤った値を渡している、または data 配列の更新と index の管理が適切に行われていない可能性があります。
    • 解決策
      • index0 以上かつ data.length - 1 以下であることを確認してください。
      • data 配列が更新される際に、index の値も適切に更新するように管理してください。
  1. data が空の配列

    • エラー
      data 配列が空の場合、スクロールする対象が存在しないため、scrollToIndex を呼び出しても何も起こりません。エラーは発生しないことが多いですが、意図した動作にはなりません。
    • 原因
      データの取得が遅れている、またはデータが存在しない状況で scrollToIndex を呼び出している可能性があります。
    • 解決策
      • data が空でないことを確認してから scrollToIndex を呼び出すようにしてください。
      • データのローディング状態などを考慮し、適切なタイミングで scrollToIndex を呼び出すようにしてください。
  2. アニメーションが期待通りに動作しない

    • 問題
      animated: true を設定してもアニメーションしない、またはアニメーションが途中で止まるなど。
    • 原因
      • 複雑なレンダリング処理が走っているなど、パフォーマンス上の問題でアニメーションがスムーズに動作しない場合があります。
      • 他のアニメーションと競合している可能性があります。
    • 解決策
      • 不要なレンダリングを避けるように最適化してください (React.memoshouldComponentUpdate など)。
      • 他のアニメーションとの競合がないか確認してください。
      • useNativeDrivertrue に設定することで、ネイティブ側でアニメーションを実行させ、パフォーマンスを向上させられる場合があります。
  3. viewOffset や viewPosition の誤った使用

    • 問題
      スクロール後のアイテムの位置が意図した場所にならない。
    • 原因
      viewOffsetviewPosition に不適切な値を設定している可能性があります。
    • 解決策
      • これらのプロパティの役割を正しく理解し、意図するレイアウトに合わせて適切な値を設定してください。
      • 特に viewOffset は、ヘッダーや他の固定要素の高さなどを考慮して設定する必要があります。
  4. 非同期処理との連携

    • 問題
      データが非同期でロードされる場合、ロード完了前に scrollToIndex を呼び出してしまうと、意図したアイテムが存在せずスクロールできないことがあります。
    • 原因
      データのロードが完了する前に scrollToIndex を呼び出している。
    • 解決策
      • データのロードが完了した後、または必要なアイテムがレンダリングされた後に scrollToIndex を呼び出すようにしてください。状態管理 (useState, Redux, Context API など) を利用して、データのロード状態を監視し、適切なタイミングで scrollToIndex を実行するように制御します。
  5. keyExtractor の不備

    • 問題
      keyExtractor が正しく設定されていない場合、アイテムの再レンダリングやスクロールの挙動に予期せぬ影響を与える可能性があります。直接 scrollToIndex のエラーとは限りませんが、関連するトラブルの原因になることがあります。
    • 原因
      keyExtractor が一意なキーを返していない、または設定されていない。
    • 解決策
      • keyExtractor が各アイテムに対して一意な文字列を返すように正しく設定してください。

トラブルシューティングのヒント

  • React Native デバッガーの利用
    React Native デバッガーを使用すると、コンポーネントの状態やプロパティの変化をより詳細に確認できます。
  • シンプルな実装で試す
    まずは簡単な FlatListscrollToIndex の実装で動作を確認し、徐々に複雑化していくことで、問題の切り分けがしやすくなります。
  • console.log の活用
    ref の状態、index の値、data の内容などを console.log で出力して、問題の原因を探ることが有効です。


基本的なスクロール

これは、ボタンを押すと特定のインデックスのアイテムまでアニメーション付きでスクロールする基本的な例です。

import React, { useRef, useState } from 'react';
import { FlatList, View, Text, Button, StyleSheet } from 'react-native';

const Example1 = () => {
  const flatListRef = useRef(null);
  const [data] = useState(Array.from({ length: 50 }, (_, index) => ({ key: String(index), value: `Item ${index}` })));

  const scrollToItem = (index) => {
    if (flatListRef.current) {
      flatListRef.current.scrollToIndex({ animated: true, index: index });
    }
  };

  return (
    <View style={styles.container}>
      <FlatList
        ref={flatListRef}
        data={data}
        renderItem={({ item }) => <Text style={styles.item}>{item.value}</Text>}
      />
      <View style={styles.buttonContainer}>
        <Button title="Go to Item 10" onPress={() => scrollToItem(10)} />
        <Button title="Go to Item 30" onPress={() => scrollToItem(30)} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  item: {
    padding: 15,
    fontSize: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    paddingVertical: 20,
  },
});

export default Example1;

解説

  • ボタンが押されると、対応するインデックスのアイテムまでスクロールします。
  • animated: true は、スクロールをアニメーションさせる設定です。
  • scrollToItem 関数は、引数として受け取った index を使って flatListRef.current.scrollToIndex() を呼び出します。
  • <FlatList> コンポーネントの ref プロパティにこの flatListRef を渡します。これにより、<FlatList> のインスタンスへの参照を flatListRef.current で取得できます。
  • useRef(null)flatListRef という ref オブジェクトを作成します。

viewOffset と viewPosition の使用

ヘッダーなどの固定要素がある場合に、スクロール後のアイテムの位置を調整する例です。

import React, { useRef, useState } from 'react';
import { FlatList, View, Text, Button, StyleSheet } from 'react-native';

const FixedHeader = () => {
  const flatListRef = useRef(null);
  const [data] = useState(Array.from({ length: 50 }, (_, index) => ({ key: String(index), value: `Item ${index}` })));
  const headerHeight = 60; // ヘッダーの高さ

  const scrollToItemWithOffset = (index) => {
    if (flatListRef.current) {
      flatListRef.current.scrollToIndex({
        animated: true,
        index: index,
        viewOffset: headerHeight, // ヘッダーの高さ分オフセット
        viewPosition: 0, // アイテムの先頭をビューポートの先頭に合わせる (ヘッダーの下)
      });
    }
  };

  return (
    <View style={styles.container}>
      <View style={[styles.header, { height: headerHeight }]}>
        <Text style={styles.headerText}>Fixed Header</Text>
      </View>
      <FlatList
        ref={flatListRef}
        data={data}
        renderItem={({ item }) => <Text style={styles.item}>{item.value}</Text>}
      />
      <View style={styles.buttonContainer}>
        <Button title="Go to Item 15" onPress={() => scrollToItemWithOffset(15)} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    backgroundColor: 'lightblue',
    justifyContent: 'center',
    alignItems: 'center',
  },
  headerText: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  item: {
    padding: 15,
    fontSize: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
  },
  buttonContainer: {
    paddingVertical: 20,
    alignItems: 'center',
  },
});

export default FixedHeader;

解説

  • viewPosition: 0 は、アイテムの先頭をビューポートの先頭に合わせることを意味します。
  • scrollToItemWithOffset 関数内で、scrollToIndexviewOffset プロパティに headerHeight を設定しています。これにより、スクロール後のアイテムの先頭がビューポートの先頭からヘッダーの高さ分だけ下にずれて表示されます。
  • headerHeight 変数でヘッダーの高さを定義します。

非同期処理との連携

データが非同期でロードされる場合に、ロード完了後にスクロールする例です。

import React, { useRef, useState, useEffect } from 'react';
import { FlatList, View, Text, Button, StyleSheet } from 'react-native';

const AsyncDataScroll = () => {
  const flatListRef = useRef(null);
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 3秒後にデータをロードするシミュレーション
    setTimeout(() => {
      const newData = Array.from({ length: 30 }, (_, index) => ({ key: String(index), value: `Async Item ${index}` }));
      setData(newData);
      setLoading(false);
    }, 3000);
  }, []);

  const scrollToMiddle = () => {
    if (flatListRef.current && data.length > 0) {
      const middleIndex = Math.floor(data.length / 2);
      flatListRef.current.scrollToIndex({ animated: true, index: middleIndex });
    }
  };

  return (
    <View style={styles.container}>
      {loading ? (
        <Text>Loading data...</Text>
      ) : (
        <FlatList
          ref={flatListRef}
          data={data}
          renderItem={({ item }) => <Text style={styles.item}>{item.value}</Text>}
        />
      )}
      {!loading && (
        <View style={styles.buttonContainer}>
          <Button title="Scroll to Middle" onPress={scrollToMiddle} />
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  item: {
    padding: 15,
    fontSize: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
  },
  buttonContainer: {
    paddingVertical: 20,
  },
});

export default AsyncDataScroll;
  • scrollToMiddle 関数は、データがロードされてから(data.length > 0 を確認)、中央のインデックスを計算してスクロールを実行します。
  • loadingtrue の間は "Loading data..." というテキストを表示し、false になったら <FlatList> とボタンを表示します。
  • useEffect フック内で、setTimeout を使って非同期のデータロードをシミュレートしています。
  • useState を使って dataloading の状態を管理します。


scrollToItem() の使用

<FlatList> には scrollToItem() というメソッドも用意されています。こちらは、インデックスではなく、直接アイテムの参照 (item) を指定してスクロールできます。

import React, { useRef, useState } from 'react';
import { FlatList, View, Text, Button, StyleSheet } from 'react-native';

const ScrollToItemExample = () => {
  const flatListRef = useRef(null);
  const [data] = useState(Array.from({ length: 50 }, (_, index) => ({ key: String(index), value: `Item ${index}` })));

  const scrollToSpecificItem = (itemToScroll) => {
    if (flatListRef.current) {
      flatListRef.current.scrollToItem({ animated: true, item: itemToScroll });
    }
  };

  const targetItem = data[25]; // 例として26番目のアイテムを取得

  return (
    <View style={styles.container}>
      <FlatList
        ref={flatListRef}
        data={data}
        renderItem={({ item }) => <Text style={styles.item}>{item.value}</Text>}
      />
      <View style={styles.buttonContainer}>
        <Button title="Go to Item 26" onPress={() => scrollToSpecificItem(targetItem)} />
      </View>
    </View>
  );
};

// styles は前の例と同じ

利点

  • アイテムのキーが変更された場合でも、アイテムオブジェクト自体への参照があればスクロールできます。
  • インデックスを意識する必要がなく、特定のアイテムオブジェクトに基づいてスクロールできます。

注意点

  • scrollToItem() は、getItemLayout プロパティが設定されている場合にパフォーマンスが向上します。getItemLayout は、与えられたインデックスのアイテムのオフセットと長さを返す関数で、これにより <FlatList> はすべてのアイテムをレンダリングせずにスクロール位置を計算できます。

scrollToOffset() の使用

<FlatList>scrollToOffset() メソッドを使うと、特定のオフセット値(ピクセル単位)までスクロールできます。

import React, { useRef } from 'react';
import { FlatList, View, Text, Button, StyleSheet } from 'react-native';

const ScrollToOffsetExample = () => {
  const flatListRef = useRef(null);
  const data = Array.from({ length: 50 }, (_, index) => ({ key: String(index), value: `Item ${index}` }));
  const itemHeight = 45; // 各アイテムの高さ (ボーダーを含む)

  const scrollToPosition = (offset) => {
    if (flatListRef.current) {
      flatListRef.current.scrollToOffset({ animated: true, offset: offset });
    }
  };

  return (
    <View style={styles.container}>
      <FlatList
        ref={flatListRef}
        data={data}
        renderItem={({ item }) => <Text style={styles.item}>{item.value}</Text>}
        getItemLayout={(data, index) => ({ length: itemHeight, offset: itemHeight * index, index })}
      />
      <View style={styles.buttonContainer}>
        <Button title="Scroll to Top (Offset 0)" onPress={() => scrollToPosition(0)} />
        <Button title="Scroll to Item 20" onPress={() => scrollToPosition(itemHeight * 20)} />
      </View>
    </View>
  );
};

// styles は前の例と同じ (item の height が 44 + border 1 = 45 に変更)

利点

  • スクロール位置を動的に計算する場合に便利です。
  • 正確なピクセル単位でのスクロール位置を指定できます。

注意点

  • パフォーマンス向上のために getItemLayout を適切に設定する必要があります。
  • アイテムの高さやレイアウトが変更されると、オフセットの計算が複雑になる可能性があります。

ScrollView と useScrollTo フックの組み合わせ (カスタム実装)

<FlatList> の代わりに <ScrollView> を使用し、react-native-hooks ライブラリの useScrollTo フックなどを利用してスクロールを制御する方法も考えられます。ただし、これは <FlatList> の仮想化によるパフォーマンス上の利点を失うため、アイテム数が少ない場合に限られます。

import React, { useRef } from 'react';
import { ScrollView, View, Text, Button, StyleSheet } from 'react-native';
import { useScrollTo } from 'react-native-hooks';

const ScrollViewExample = () => {
  const scrollViewRef = useRef(null);
  const scrollTo = useScrollTo(scrollViewRef);
  const data = Array.from({ length: 50 }, (_, index) => ({ key: String(index), value: `Item ${index}` }));
  const itemHeight = 45;

  const scrollToItemIndex = (index) => {
    scrollTo({ y: itemHeight * index, animated: true });
  };

  return (
    <View style={styles.container}>
      <ScrollView ref={scrollViewRef}>
        {data.map((item) => (
          <Text key={item.key} style={styles.item}>{item.value}</Text>
        ))}
      </ScrollView>
      <View style={styles.buttonContainer}>
        <Button title="Go to Item 15" onPress={() => scrollToItemIndex(15)} />
      </View>
    </View>
  );
};

// styles は前の例と同じ

利点

  • より柔軟なスクロール制御が可能になる場合があります。

注意点

  • <FlatList> が提供する仮想化や最適化の恩恵を受けられません。
  • アイテム数が多くなるとパフォーマンスが著しく低下します。

状態管理と useEffect を組み合わせた間接的なスクロール

特定のアイテムを表示したい状態を管理し、その状態の変化を useEffect で監視して、<FlatList>scrollToIndexscrollToItem を間接的に呼び出す方法です。

import React, { useRef, useState, useEffect } from 'react';
import { FlatList, View, Text, Button, StyleSheet } from 'react-native';

const IndirectScrollExample = () => {
  const flatListRef = useRef(null);
  const [data] = useState(Array.from({ length: 50 }, (_, index) => ({ key: String(index), value: `Item ${index}` })));
  const [targetIndex, setTargetIndex] = useState(null);

  useEffect(() => {
    if (targetIndex !== null && flatListRef.current) {
      flatListRef.current.scrollToIndex({ animated: true, index: targetIndex });
      setTargetIndex(null); // スクロール後にリセット
    }
  }, [targetIndex, flatListRef]);

  const goToItem = (index) => {
    setTargetIndex(index);
  };

  return (
    <View style={styles.container}>
      <FlatList
        ref={flatListRef}
        data={data}
        renderItem={({ item }) => <Text style={styles.item}>{item.value}</Text>}
      />
      <View style={styles.buttonContainer}>
        <Button title="Go to Item 5" onPress={() => goToItem(5)} />
        <Button title="Go to Item 25" onPress={() => goToItem(25)} />
      </View>
    </View>
  );
};

// styles は前の例と同じ

利点

  • 他の状態変化と連動したスクロール処理が可能です。
  • スクロールのロジックをコンポーネントの状態とライフサイクルに統合できます。

注意点

  • 直接的なメソッド呼び出しに比べて、やや複雑になる場合があります。

FlatList.scrollToIndex() はインデックスに基づいたスクロールの主要な方法ですが、scrollToItem() (アイテム参照ベース)、scrollToOffset() (ピクセルオフセットベース) などの代替手段も存在します。また、<ScrollView> を使用したり、状態管理と useEffect を組み合わせることで、間接的にスクロールを制御することも可能です。