⚡️

Next.jsチーム謹製のData Fetcher・useSWRについてのまとめ

2023/12/19に公開

TL;DR

  • Next.js チーム謹製の Data Fetcher / useSWR についてのまとめです
  • 最近実務でuseSWRを使う機会があったので、基本的な使い方についてドキュメントを読んだ内容のメモ的な記事です

What's SWR

データ取得のための React Hooks ライブラリです。

公式ドキュメントによると、

SWR では、 コンポーネントはデータの更新を継続的かつ自動的に受け取ることができます。
そして、 UI は常に高速でリアクティブなモノになります。

だそうです。

公式ドキュメントの冒頭で記載されているサンプルコードを見てみます。

import useSWR from 'swr'

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

useSWR フック

key文字列とfetcher関数を受け取ります。

keyはデータの一意な識別子で、通常は API の URL を想定しています。

fetcherは引数にkeyを受け取りデータを返す任意の非同期関数で、ネイティブの fetch などを使うことができます。

このフックはdata,error,isLodingの 3 つを返してくれるので、リクエストの状態に基づいて表示を切り替えることが可能です。

再利用可能にする

関数に useSWR を内包することで、再利用可能となります。

function useUser (id) {
  const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading,
    isError: error
  }
}

useEffect に対する優位性

例えば親コンポーネントないで初期表示時にuserの情報を取得し、state にセットした上で子コンポーネントに渡しているとします。

この場合であれば親子なので良いですが、場合によっては孫・ひ孫に至るまで props にてuserをバケツリレーする必要があります。

import { useSearchParams } from "next/navigation";

// 親コンポーネント
function Page () {
  const [user, setUser] = useState(null)
  const searchParams = useSearchParams();
  const id = searchParams.get("userId");

  // データの取得
  useEffect(() => {
    fetch('/api/user/${id}')
      .then(res => res.json())
      .then(data => setUser(data))
  }, [id])

  // グローバルなローディング状態
  if (!user) return <Spinner/>

  return <div>
    <Navbar user={user} />
    <Content user={user} />
  </div>
}

// 子コンポーネント群

function Navbar ({ user }) {
  return <div>
    ...
    <Avatar user={user} />
  </div>
}

function Content ({ user }) {
  return <h1>Welcome back, {user.name}</h1>
}

function Avatar ({ user }) {
  return <img src={user.avatar} alt={user.name} />
}

contextを使うことでバケツリレーは避けられますが、レンダリング効率の問題や、動的なコンテンツの問題(トップレベルのコンポーネントが子コンポーネントに過不足なくデータを渡せているかどうか?)が残ります。

しかし、再利用可能となった useSWR を使うことでこの問題を解決することができます。

先ほどのコードをリファクタしたものを見てみると、

import { useSearchParams } from "next/navigation";

// useSWRを内包したフックを作る
function useUser () {
  const searchParams = useSearchParams();
  const id = searchParams.get("userId");
  const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading,
    isError: error
  }
}

// 親コンポーネント
function Page () {
  return <div>
    <Navbar />
    <Content />
  </div>
}

// 子コンポーネント群
function Navbar () {
  return <div>
    ...
    <Avatar />
  </div>
}

function Content () {
  const { user, isLoading } = useUser()
  if (isLoading) return <Spinner />
  return <h1>Welcome back, {user.name}</h1>
}

function Avatar () {
  const { user, isLoading } = useUser()
  if (isLoading) return <Spinner />
  return <img src={user.avatar} alt={user.name} />
}

これによりデータ fetch は各子コンポーネントに結合され、データはコンポーネントごとに独立します。

しかし、一見コンポーネントがレンダリングされるごとにデータ fetch が行われるように見えますが、その心配はありません。

なぜなら、useSWR は同じkeyを使うことでリクエストは自動的に重複排除、キャッシュ、共有されるからです。

また、ユーザーのフォーカスやネットワークの再接続時にアプリケーションがデータを再取得できるようにもなります。

https://swr.vercel.app/ja/docs/revalidation

fetcher に複数の引数を渡す

例えば API にtokenを付けて fetch を行う場合に、URL と token をfetcherに渡すというシナリオが考えられます。

その場合、配列に格納した上でfetcherへ値を引き渡します。

const { data: user } = useSWR(['/api/user', token], ([url, token]) => fetchWithToken(url, token))

オブジェクトの受け渡し

同様にオブジェクトをfetcherへ引き渡すことも可能です。

const { data: orders } = useSWR({ url: '/api/orders', args: user }, fetcher)

条件付き fetch

keyに null が渡ってきた場合、useSWR はリクエストを開始しません。

const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)

おわりに

useEffect との優位性のところが素晴らしいと思いました。

error, isLoadingも返してくれるので、データ fetch に必要な情報の痒いところに手が届いてる感がすごいです。

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。

よろしければ Conpass からメンバー登録よろしくお願いいたします。

https://blessingsoftware.connpass.com/

Discussion