Reactでライブラリなしで、スムーズにスクロールしてみたサンプル

Reactを使用して、ボタンを押したときに指定のDOMの位置までスムーズにアニメーションしながらスクロールするサンプルを作成してみました

スムーズにスクロールさせる方法は色々あると思いますが、私は以下の方法を試してみました。

- 方法1 `scrollTo()` を使う (※IEとSafarではうまく動かなかった)
- 方法2 `requestAnimationFrame` を使う

# `scrollTo()`

https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo

`scrollTo()`を呼び出すときにオプションで`behavior: 'smooth'`と指定することでスムーズにアニメーションしながらスクロースしてくれました。
※IEとSafariではスクロールするけど、アニメーションしないで、一瞬で対象の位置まで行ってしまいました

<p class="codepen" data-height="576" data-theme-id="default" data-default-tab="result" data-user="okumurakengo" data-slug-hash="MWWzqQG" style="height: 576px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="react smooth scroll">
  <span>See the Pen <a href="https://codepen.io/okumurakengo/pen/MWWzqQG">
  react smooth scroll</a> by 奥村健吾 (<a href="https://codepen.io/okumurakengo">@okumurakengo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

```jsx
const styles = {
  width: '500px',
  height: '500px',
};

function App() {
  const div1 = React.useRef()
  const div2 = React.useRef()
  const div3 = React.useRef()

  /**
   * @param {HTMLElement} target 
   */
  function smoothScroll(target) {
    const { top } = target.getBoundingClientRect()
    window.scrollTo({
      top: top + window.pageYOffset,
      behavior: "smooth"
    });
  }

  return (
    <div>
      <div ref={div1} style={{ background: 'yellowgreen', ...styles }}>
        <p><button onClick={() => smoothScroll(div1.current)}>yellowgreen</button></p>
        <p><button onClick={() => smoothScroll(div2.current)}>seagreen</button></p>
        <p><button onClick={() => smoothScroll(div3.current)}>skyblue</button></p>
      </div>

      <div ref={div2} style={{ background: 'seagreen', ...styles }}>
        <p><button onClick={() => smoothScroll(div1.current)}>yellowgreen</button></p>
        <p><button onClick={() => smoothScroll(div2.current)}>seagreen</button></p>
        <p><button onClick={() => smoothScroll(div3.current)}>skyblue</button></p>
      </div>

      <div ref={div3} style={{ background: 'skyblue', ...styles }}>
        <p><button onClick={() => smoothScroll(div1.current)}>yellowgreen</button></p>
        <p><button onClick={() => smoothScroll(div2.current)}>seagreen</button></p>
        <p><button onClick={() => smoothScroll(div3.current)}>skyblue</button></p>
      </div>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'));
```


# `requestAnimationFrame`

スクロール位置の判定は自分でしないといけないですが、以下のようにするとスムーズにアニメーションしながらスクロースしてくれました、
私の環境ではIE、Safariでも動作するのを確認できました。

<p class="codepen" data-height="589" data-theme-id="default" data-default-tab="result" data-user="okumurakengo" data-slug-hash="ExxOOWm" style="height: 589px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="react smooth scroll (requestAnimationFrame)">
  <span>See the Pen <a href="https://codepen.io/okumurakengo/pen/ExxOOWm">
  react smooth scroll (requestAnimationFrame)</a> by 奥村健吾 (<a href="https://codepen.io/okumurakengo">@okumurakengo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

```jsx
/**
 * 下にスクロールするピクセル値の間隔
 * @constant
 * @type {number}
 */
const INTERVAL = 15;

const styles = {
  width: '500px',
  height: '500px',
};

function App() {
  const requestRef = React.useRef()
  const targetTopRef = React.useRef()
  const startYOffset = React.useRef()

  const div1 = React.useRef()
  const div2 = React.useRef()
  const div3 = React.useRef()

  /**
   * @param {HTMLElement} target 
   * @returns {void}
   */
  function smoothScroll(target) {
    // スクロール対象の位置を取得
    ({ top: targetTopRef.current } = target.getBoundingClientRect())

    // スクロール開始位置を取得
    startYOffset.current = window.pageYOffset

    // アニメーション開始
    animate()
  }

  /**
   * @returns {void}
   */
  function animate() {
    requestRef.current = requestAnimationFrame(animate);

    if (targetTopRef.current > 0) {
      // 下にスクロールする場合
      if (((startYOffset.current + targetTopRef.current) - window.pageYOffset) < INTERVAL) {
        // 残りのスクロールするピクセル値が、INTERVALの値より少ない場合は
        // 行き過ぎないようにスクロール値を調整
        window.scrollTo(0, window.pageYOffset + ((startYOffset.current + targetTopRef.current) - window.pageYOffset))
      } else {
        // 下にスクロール
        window.scrollTo(0, window.pageYOffset + INTERVAL)
      }

      if (((startYOffset.current + targetTopRef.current) <= window.pageYOffset)
      || (document.documentElement.scrollTop + window.innerHeight) >= (document.documentElement.offsetHeight)) {
        // スクロールの対象まできたか、ウィンドウの最下部にきたらスクロール終了
        cancelAnimationFrame(requestRef.current);
      }
    } else {
      // 上にスクロールする場合
      if (-((startYOffset.current + targetTopRef.current) - window.pageYOffset) < INTERVAL) {
        // 残りのスクロールするピクセル値が、INTERVALの値より少ない場合は
        // 行き過ぎないようにスクロール値を調整
        window.scrollTo(0, window.pageYOffset + ((startYOffset.current + targetTopRef.current) - window.pageYOffset))
      } else {
        // 上にスクロール
        window.scrollTo(0, window.pageYOffset - INTERVAL)
      }

      if ((startYOffset.current + targetTopRef.current) >= window.pageYOffset) {
        // スクロールの対象まできたらスクロール終了
        cancelAnimationFrame(requestRef.current);
      }
    }
  }

  return (
    <div>
      <div ref={div1} style={{ background: 'yellowgreen', ...styles }}>
        <p><button onClick={() => smoothScroll(div1.current)}>yellowgreen</button></p>
        <p><button onClick={() => smoothScroll(div2.current)}>seagreen</button></p>
        <p><button onClick={() => smoothScroll(div3.current)}>skyblue</button></p>
      </div>

      <div ref={div2} style={{ background: 'seagreen', ...styles }}>
        <p><button onClick={() => smoothScroll(div1.current)}>yellowgreen</button></p>
        <p><button onClick={() => smoothScroll(div2.current)}>seagreen</button></p>
        <p><button onClick={() => smoothScroll(div3.current)}>skyblue</button></p>
      </div>

      <div ref={div3} style={{ background: 'skyblue', ...styles }}>
        <p><button onClick={() => smoothScroll(div1.current)}>yellowgreen</button></p>
        <p><button onClick={() => smoothScroll(div2.current)}>seagreen</button></p>
        <p><button onClick={() => smoothScroll(div3.current)}>skyblue</button></p>
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
```