「zeit/swr」のpagination exampleでScroll Position Restoreを試す

こちらは[Next.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/next-js)の22日目の記事です。

## はじめに
例えばYoutubeをスマホで見ていたときに、↓のように一度ページ遷移をしてからブラウザバックしたときにスクロール位置が一番上まで戻ってしまった経験はないでしょうか。

![cap1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6159/4f7cca3d-5fde-0e6c-c747-1fa7a2b2f5d9.gif)

これはSPAが抱える課題の1つで、解決するためには「Scroll Position Restore」と呼ばれる「スクロール位置の保持」とその「リストア（復元）」が必要です。

また、それ以外にも「もっと読む」などで画面内に非同期で要素が追加された場合はその要素も合わせて復元しないと正しいスクロール位置には戻れません。これには複雑なStateの管理も必要になってきます。

このように「Scroll Position Restore」を実現するにはいくつか超えなくてはいけない壁があります。

## zeit/swr の登場
2019年10月、 Next.jsやNowの開発元であるZeitから[「zeit/swr」](https://swr.now.sh/)というライブラリが発表されました。
そして、この[「zeit/swr」](https://swr.now.sh/)の機能の1つにある「Pagination」が「Scroll Position Restore」を実現するための機能となっています！

swr（Stale-While-Revalidate） とは主にキャッシュ周りの用語なのでこのライブラリもキャッシュ周りのハンドリングも含むdata fetchのReact Hooksライブラリとなっています。[^1]

簡単にやってくれることを書くと、「最初にキャッシュした古いデータを見せておいて、その裏で最新のデータをfetchしてキャッシュと差し替える」といった感じかなと思います。[^2]

[^1]: このライブラリきっかけでswr自体を勉強し直しました。。[こちらの記事](https://blog.jxck.io/entries/2016-04-16/stale-while-revalidate.html)が分かりやすかったです。

[^2]: 詳しくは[こちらの記事](https://tech.mercari.com/entry/2019/12/17/130000)や[こちらの記事](https://qiita.com/abyssparanoia/items/53c4c77e29c84b9977fd)をおすすめします！

## 早速、Paginationを動かしてみる
Next.jsもそうですが、リポジトリ内にexamplesというサンプル集があり、そこにPaginationのexampleもあったので今回はそれを動かしてみます。

[swr/examples/pagination at master · zeit/swr](https://github.com/zeit/swr/tree/master/examples/pagination)

READMEに書いてあるとおり、Setupを終わらせます（このサンプル自体もNext.jsが使われていました）。

```sh
$ curl https://codeload.github.com/zeit/swr/tar.gz/master | tar -xz --strip=2 swr-master/examples/pagination
$ cd pagination
$ yarn
$ yarn dev
```

起動したサーバーにアクセスすると以下のページが出てきます。「load more」で読み込んだ要素が、ページ遷移をしてブラウザバックをしても表示されていることがわかると思います！また、スクロール位置も保持されていそうですね。[^3]

[^3]: 分かりやすくするために1度に読む込むデータ数を3件から10件に変更しています。

![cap2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6159/f0a67ff6-4c48-2d42-3144-078c53cf0b65.gif)

実装はこんな感じです。

```js
import fetch from '../libs/fetch'
import Link from 'next/link'

import useSWR, { useSWRPages } from 'swr'

export default () => {
  const {
    pages,
    isLoadingMore,
    isReachingEnd,
    loadMore
  } = useSWRPages(
    // page key
    'demo-page',

    // page component
    ({ offset, withSWR }) => {
      const { data: projects } = withSWR(
        // use the wrapper to wrap the *pagination API SWR*
        useSWR('/api/projects?offset=' + (offset || 0), fetch)
      )
      // you can still use other SWRs outside

      if (!projects) {
        return <p>loading</p>
      }

      return projects.map(project => 
        <p key={project.id}>{project.name}</p>
      )
    },

    // one page's SWR => offset of next page
    ({ data: projects }) => {
      return projects && projects.length
        ? projects[projects.length - 1].id + 1
        : null
    },

    // deps of the page component
    []
  )

  return <div>
    <h1>Pagination (offset from data)</h1>
    {pages}
    <button onClick={loadMore} disabled={isReachingEnd || isLoadingMore}>
      {isLoadingMore ? '. . .' : isReachingEnd ? 'no more data' : 'load more'}
    </button>
    <hr />
    <Link href="/page-index"><a>page index based pagination →</a></Link><br/>
    <Link href="/about"><a>go to another page →</a></Link>
  </div>
}
```

`useSWRPages()`の引数として、キャッシュキーとしても使われるページ名、実際にfetchしたデータを使って生成するComponent、次ページのoffsetなどを設定しています。

ここの説明は[コード側にシーケンス図があった](https://github.com/zeit/swr/blob/d4db9c4ccad55332013be4f9b25ac3a3f9db4127/src/use-swr-pages.tsx#L11)のでそちらを見るとより分かりやすいかなと思います。

少し触ってみたりissueなども見たところ、まだスクロール位置の復元がやや不完全であったり、キャッシュのストアを触ろうとしたときに不自由さはあるものの、シンプルなアプリケーションであれば比較的簡単に利用できそうな印象でした。

## さいごに
zeit/swrはまだまだ発表されたばかりで利用実績も多くありませんが、頻繁に開発されており、何より開発元がzeitであるため今後ますますNext.jsとうまく協調して進化していくはずです。Nextをお使いの方はぜひその動向に注目してみてください！

以上、22日目の記事でした！