パフォーマンス改善も!React Native FlatList で scrollToOffset() を使いこなす

2025-05-31

基本的な使い方

FlatList コンポーネントのインスタンス(ref を使って取得します)に対して、以下のように scrollToOffset() メソッドを呼び出します。

this.flatListRef.scrollToOffset({ offset: 100, animated: true });

この例では、リストの先頭から 100 ピクセル下(または右、スクロール方向によります)の位置まで、アニメーションを伴ってスクロールします。

引数

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

  • viewPosition (number, オプション): (iOS のみ) スクロール後のアイテムの表示位置を 0 (先頭) から 1 (末尾) の間の値で指定します。例えば 0.5 を指定すると、アイテムを画面の中央に表示しようとします。デフォルトは 0 です。
  • viewOffset (number, オプション): (iOS のみ) スクロール後のアイテムの先頭位置に追加するオフセット値を指定します。例えば、アイテムの先頭を完全に画面の端に合わせるのではなく、少しスペースを空けて表示したい場合に利用します。デフォルトは 0 です。
  • animated (boolean, オプション): スクロールをアニメーションさせるかどうかを指定します。true を指定するとアニメーションしながらスクロールし、false を指定すると瞬時にスクロールします。デフォルトは true です。
  • offset (number): スクロール先のオフセット値を指定します。リストの開始位置からの距離をピクセル単位で指定します。垂直スクロールの場合は上からの距離、水平スクロールの場合は左からの距離になります。必須の引数です。

利用シーン

scrollToOffset() は、以下のような場合に便利です。

  • 連続的なアニメーション
    時間経過や何らかのイベントに応じて、リストを段階的にスクロールさせるようなアニメーションの実装。
  • ボタン操作による特定位置への移動
    「トップへ戻る」ボタンや、特定のセクションへジャンプするボタンの実装など。
  • 初期表示位置の調整
    リストの初期表示位置を先頭以外にしたい場合。
  • 特定のアイテムへのプログラム的なスクロール
    例えば、検索結果の特定のアイテムを自動的に表示したい場合など。
  • アニメーション (animated: true) は、パフォーマンスに影響を与える可能性があるため、必要に応じて使い分けることが重要です。
  • スクロール先の offset 値がリストの範囲外である場合、リストは可能な範囲で最も近い位置までスクロールします。
  • scrollToOffset() を呼び出すためには、FlatList コンポーネントの ref を取得している必要があります。これは、useRef フックや createRef メソッドを使って実現できます。


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

    • 原因
      FlatList コンポーネントに ref が正しく設定されていない、または scrollToOffset() を呼び出すタイミングが FlatList のインスタンスがまだ作成されていない時点である可能性があります。
    • 解決策
      • FlatList コンポーネントに ref を正しく設定しているか確認してください。関数コンポーネントの場合は useRef フックを使用し、クラスコンポーネントの場合は createRef を使用します。
      • scrollToOffset() を呼び出すタイミングを、FlatList がマウントされた後、または必要なデータがロードされた後など、適切なライフサイクルメソッドやイベントハンドラー内で行うようにしてください。例えば、componentDidMount (クラスコンポーネント) や useEffect (関数コンポーネント) 内で、必要に応じて setTimeout などを使って遅延させることも有効です。
    // 関数コンポーネントの例
    import React, { useRef, useEffect } from 'react';
    import { FlatList, View, Text } from 'react-native';
    
    const MyList = () => {
      const flatListRef = useRef(null);
      const data = [...Array(100).keys()].map(i => ({ key: i.toString(), text: `Item ${i}` }));
    
      useEffect(() => {
        // データがロードされた後など、適切なタイミングで呼び出す
        if (data.length > 0 && flatListRef.current) {
          flatListRef.current.scrollToOffset({ offset: 500, animated: true });
        }
      }, [data]); // data が変更されたときにも実行されるように依存配列に追加
    
      return (
        <FlatList
          ref={flatListRef}
          data={data}
          renderItem={({ item }) => <Text style={{ padding: 20 }}>{item.text}</Text>}
        />
      );
    };
    
  1. 意図したオフセット位置にスクロールしない

    • 原因
      指定した offset 値がリストの範囲外である、またはリストのレイアウトがまだ完了していない可能性があります。
    • 解決策
      • 指定する offset 値が、リストの実際のコンテンツサイズを超えていないか確認してください。
      • リストのレイアウトが完了する前に scrollToOffset() を呼び出している場合、意図した位置にスクロールしないことがあります。onLayout プロパティを使ってレイアウト完了を検知し、その後に scrollToOffset() を呼び出すことを検討してください。ただし、頻繁なレイアウト計算はパフォーマンスに影響を与える可能性があるため注意が必要です。
  2. アニメーションが期待通りに動作しない

    • 原因
      animated: false になっている、または他のスタイルやアニメーションと競合している可能性があります。
    • 解決策
      • animated プロパティが true に設定されていることを確認してください。
      • 他のアニメーションライブラリやスタイル設定がスクロールアニメーションを妨げていないか確認してください。
  3. 垂直スクロールのリストで水平方向のオフセットを指定している、またはその逆

    • 原因
      FlatListhorizontal プロパティの設定と、offset の方向が一致していない可能性があります。
    • 解決策
      • 垂直スクロールのリスト (horizontal={false} またはデフォルト) では垂直方向のオフセットを、水平スクロールのリスト (horizontal={true}) では水平方向のオフセットを指定してください。
  4. パフォーマンスの問題

    • 原因
      大量のデータを扱うリストで頻繁に scrollToOffset() をアニメーション付きで呼び出すと、パフォーマンスに影響を与える可能性があります。
    • 解決策
      • 不必要に頻繁な scrollToOffset() の呼び出しを避けてください。
      • アニメーションが不要な場合は animated: false を使用することを検討してください。
      • リストのアイテムのレンダリング最適化(shouldComponentUpdateReact.memo など)も重要です。
  5. iOS 特有の問題 (viewOffset, viewPosition)

    • 原因
      viewOffsetviewPosition は iOS のみに適用されるプロパティです。Android でこれらのプロパティを使用しても効果はありません。
    • 解決策
      • iOS と Android で異なる動作をさせる必要がある場合は、プラットフォームによる条件分岐を行いましょう。

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

  • React Native デバッガーの利用
    React Native デバッガーの要素インスペクタなどを利用して、コンポーネントの状態やスタイルを確認することも有効です。
  • シンプルな例で試す
    まずは簡単なリストと scrollToOffset() の基本的な動作を確認するコードを作成し、問題の切り分けを行いましょう。
  • console.log の活用
    ref が正しく取得できているか、offset の値が意図したものになっているかなどを console.log で確認しましょう。


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

const items = Array.from({ length: 100 }, (_, index) => ({ key: index.toString(), text: `アイテム ${index + 1}` }));

const ScrollToOffsetExample = () => {
  const flatListRef = useRef(null);

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

  return (
    <View style={styles.container}>
      <FlatList
        ref={flatListRef}
        data={items}
        renderItem={({ item }) => (
          <View style={styles.item}>
            <Text>{item.text}</Text>
          </View>
        )}
      />
      <Button title="オフセット 500 までスクロール" onPress={handleScrollToOffset} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 20,
  },
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
});

export default ScrollToOffsetExample;

コードの説明

  1. useRef(null) を使って、FlatList コンポーネントへの参照 (flatListRef) を作成します。初期値は null です。
  2. FlatList コンポーネントの ref プロパティに flatListRef を設定します。これにより、FlatList のインスタンスへのアクセスが可能になります。
  3. handleScrollToOffset 関数は、ボタンが押されたときに実行されます。
  4. 関数内では、まず flatListRef.current が存在するかどうかを確認します。
  5. 存在する場合、flatListRef.current.scrollToOffset() を呼び出し、offset にスクロール先のオフセット値(ここでは 500 ピクセル)、animatedtrue を指定してアニメーション付きでスクロールします。

この例では、特定のインデックスのアイテムがリストの先頭に表示されるようにスクロールします。アイテムの高さが一定であることを前提としています。

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

const ITEM_HEIGHT = 68; // アイテムの高さを固定値で定義
const items = Array.from({ length: 100 }, (_, index) => ({ key: index.toString(), text: `アイテム ${index + 1}` }));

const ScrollToItemTopExample = () => {
  const flatListRef = useRef(null);
  const scrollToItemIndex = 20; // スクロールしたいアイテムのインデックス

  const handleScrollToItemTop = () => {
    if (flatListRef.current) {
      flatListRef.current.scrollToOffset({ offset: scrollToItemIndex * ITEM_HEIGHT, animated: true });
    }
  };

  return (
    <View style={styles.container}>
      <FlatList
        ref={flatListRef}
        data={items}
        renderItem={({ item }) => (
          <View style={styles.item}>
            <Text>{item.text}</Text>
          </View>
        )}
      />
      <Button title={`アイテム ${scrollToItemIndex + 1} の先頭へスクロール`} onPress={handleScrollToItemTop} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 20,
  },
  item: {
    backgroundColor: '#aaffaa',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
    height: ITEM_HEIGHT, // アイテムの高さを指定
  },
});

export default ScrollToItemTopExample;

コードの説明

  1. ITEM_HEIGHT という定数でアイテムの高さを定義します。
  2. スクロールしたいアイテムのインデックス (scrollToItemIndex) を設定します。
  3. handleScrollToItemTop 関数では、スクロール先のオフセット値を scrollToItemIndex * ITEM_HEIGHT で計算します。これにより、指定したインデックスのアイテムの先頭がリストの上端にくるようにスクロールします。
import React, { useRef } from 'react';
import { FlatList, View, Text, Button, StyleSheet } from 'react-native';

const items = Array.from({ length: 10 }, (_, index) => ({ key: index.toString(), text: `アイテム ${index + 1}` }));

const HorizontalScrollToOffsetExample = () => {
  const flatListRef = useRef(null);

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

  return (
    <View style={styles.container}>
      <FlatList
        ref={flatListRef}
        data={items}
        horizontal={true}
        renderItem={({ item }) => (
          <View style={styles.horizontalItem}>
            <Text>{item.text}</Text>
          </View>
        )}
      />
      <Button title="水平オフセット 300 までスクロール" onPress={handleScrollToOffset} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 20,
  },
  horizontalItem: {
    backgroundColor: '#add8e6',
    padding: 20,
    marginHorizontal: 10,
    width: 150,
    height: 100,
  },
});

export default HorizontalScrollToOffsetExample;
  1. FlatListhorizontal プロパティを true に設定することで、水平スクロールするリストを作成します。
  2. scrollToOffsetoffset に指定する値は、水平方向のオフセットになります。


scrollToIndex()

  • 注意点
    大量のアイテムがあるリストで頻繁に scrollToIndex() を呼び出すと、パフォーマンスに影響が出る可能性があります。

  • 利点
    アイテムのインデックスに基づいてスクロールできるため、アイテムの高さが不定でも正確に目的のアイテムを表示しやすい。

  • 引数

    • index (number): スクロール先のアイテムのインデックス(0から始まる)。必須
    • animated (boolean, オプション): アニメーションの有無。デフォルトは true
    • viewOffset (number, オプション): (iOS のみ) スクロール後のアイテムの先頭に追加するオフセット。デフォルトは 0
    • viewPosition (number, オプション): (iOS のみ) スクロール後のアイテムの表示位置 (0: 先頭, 1: 末尾)。デフォルトは 0
    • itemVisiblePercentThreshold (number, オプション): スクロール完了とみなすためのアイテムの可視割合 (0〜100)。デフォルトは 0
  • this.flatListRef.scrollToIndex({ index: 10, animated: true, viewOffset: 0, viewPosition: 0 });
    

scrollToItem()

  • 注意点
    item に渡すオブジェクトが data 配列内の要素と厳密に一致している必要があります(特に key)。

  • 利点
    アイテムオブジェクト自体を指定できるため、インデックスを管理する必要がない。

  • 引数

    • item (object): スクロール先のアイテムオブジェクト。必須。このオブジェクトは data 配列内の要素と一致する必要があります。特に key プロパティが重要です。
    • animated (boolean, オプション): アニメーションの有無。デフォルトは true
    • viewOffset (number, オプション): (iOS のみ) スクロール後のアイテムの先頭に追加するオフセット。デフォルトは 0
    • viewPosition (number, オプション): (iOS のみ) スクロール後のアイテムの表示位置 (0: 先頭, 1: 末尾)。デフォルトは 0
    • itemVisiblePercentThreshold (number, オプション): スクロール完了とみなすためのアイテムの可視割合 (0〜100)。デフォルトは 0
  • 使い方

    const targetItem = this.state.data[5]; // 例: スクロールしたいアイテムのデータ
    this.flatListRef.scrollToItem({ item: targetItem, animated: true, viewOffset: 0, viewPosition: 0 });
    

useImperativeHandle (関数コンポーネントの場合)

  • 注意点
    useImperativeHandle の過度な使用は、React のデータフローの原則から外れる可能性があるため、慎重に使用する必要があります。

  • 利点
    親コンポーネントから FlatList の内部実装を直接公開せずに、特定の機能を提供できるため、コンポーネントの分離と再利用性が向上します。

  • 使い方

    import React, { useRef, useImperativeHandle, forwardRef } from 'react';
    import { FlatList, View, Text, Button, StyleSheet } from 'react-native';
    
    const MyFlatList = forwardRef((props, ref) => {
      const flatListRef = useRef(null);
    
      useImperativeHandle(ref, () => ({
        scrollToSpecificOffset: (offset, animated) => {
          flatListRef.current?.scrollToOffset({ offset, animated });
        },
        // 他のカスタムメソッドも公開できる
      }));
    
      return (
        <FlatList
          ref={flatListRef}
          data={props.data}
          renderItem={({ item }) => <Text style={styles.item}>{item.text}</Text>}
        />
      );
    });
    
    const ParentComponent = () => {
      const flatListRef = useRef(null);
      const data = [...Array(50).keys()].map(i => ({ key: i.toString(), text: `Item ${i}` }));
    
      const handleScroll = () => {
        flatListRef.current?.scrollToSpecificOffset(300, true);
      };
    
      return (
        <View style={{ flex: 1 }}>
          <MyFlatList ref={flatListRef} data={data} />
          <Button title="Scroll to Offset 300" onPress={handleScroll} />
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      item: { padding: 10, borderBottomWidth: 1, borderColor: '#ccc' },
    });
    
    export default ParentComponent;
    

ScrollView を使用する (単純なリストの場合)

  • 注意点
    大量のデータを扱う場合はパフォーマンスが悪化するため、FlatList のような仮想化されたリストを使用するべきです。

  • 利点
    シンプルなリストであれば実装が容易。

  • 使い方

    import React, { useRef } from 'react';
    import { ScrollView, View, Text, Button, StyleSheet } from 'react-native';
    
    const items = [...Array(20).keys()].map(i => ({ key: i.toString(), text: `Item ${i}` }));
    
    const ScrollViewExample = () => {
      const scrollViewRef = useRef(null);
    
      const handleScrollTo = () => {
        scrollViewRef.current?.scrollTo({ y: 200, animated: true }); // 垂直方向
        // scrollViewRef.current?.scrollTo({ x: 100, animated: true }); // 水平方向
      };
    
      return (
        <View style={{ flex: 1 }}>
          <ScrollView ref={scrollViewRef}>
            {items.map(item => (
              <Text key={item.key} style={styles.item}>{item.text}</Text>
            ))}
          </ScrollView>
          <Button title="Scroll to Y: 200" onPress={handleScrollTo} />
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      item: { padding: 10, borderBottomWidth: 1, borderColor: '#ccc' },
    });
    
    export default ScrollViewExample;
    

どの方法を選ぶべきか

  • ピクセル単位で正確なオフセット位置にスクロールしたい場合
    scrollToOffset()
  • データ量が少ない単純なリストの場合
    ScrollView とその scrollTo メソッドも検討できる
  • 親コンポーネントから FlatList のスクロールを間接的に制御したい場合
    useImperativeHandle (関数コンポーネント)
  • 特定のデータオブジェクトのアイテムにスクロールしたい場合
    scrollToItem()
  • 特定のインデックスのアイテムにスクロールしたい場合
    scrollToIndex()