【React】データ更新時にスクロールを固定する

こんにちは、フリーランスエンジニアの太田雅昭です。

更新の前後でスクロール位置の固定

スクロール位置を固定するには、単純に考えれば以下のような流れとなります。

  • 更新前のscrollHeightを保存
  • 更新実行
  • 現在のscrollHeightと更新前の値を計算
  • 差分位置までスクロール

しかし実際はReactだとレンダリングのタイミングなども考慮しないといけません。計算のタイミングで、レンダリングが終わっている保証がないためです。そのため、下記のように修正します。

  • 更新前のscrollHeightを保存
  • 更新実行
  • 下記を300ms間繰り返す
    • 現在のscrollHeightと更新前の値を計算
    • 差分位置までスクロール

またレンダリングを走らせるためにrequestAnimationFrameを使う必要もあります。

コード

下記のようになります。

function ScrollSample() {
  // スクロール要素の参照
  const scrollRef = useRef<HTMLDivElement>(null);

  // データ取得関数
  const refetch = () => new Promise<void>(resolve => setTimeout(resolve, 1000))

  // スクロール位置を保持する関数
  const keepScrollPosition = (params: { prevHeight: number }) =>
    const timeout = 300;
    new Promise<void>((resolve) => {
      const { prevHeight } = params;
      const start = performance.now();
      const tick = () => {
        const el = scrollRef?.current;
        if (!el) return resolve();
        if (!el.isConnected) return resolve();
        if (performance.now() - start > timeout) return resolve();
        el.scrollTo({
          top: el.scrollHeight - prevHeight,
          behavior: 'instant',
        })
        requestAnimationFrame(tick);
      };
      requestAnimationFrame(tick);
    });

  // 更新実行
  const handleClick = async () => {
    const el = scrollRef?.current;
    if (!el) return;
    const prevHeight = el.scrollHeight;
    await refetch();
    await keepScrollPosition({ prevHeight });
  }

  return <>
    <div ref={scrollRef}>
      {/* ここにデータを入れる */}
    </div>
    <button onClick={handleClick} >更新実行</button>
  </>
}