https://zenn.dev/yodaka/articles/f5f93877b7a19f#ハマりどころ

個人的に理解が浅くてハマったところを紹介する。

function useUserBySWR() {
    const { data, error } = useSWR('/api/users/123', fetcher)
    const loading = !data

    return {
        data,
        error,
	loading
    }
}

ハマりどころ

ここでまずハマるのが、SWRの方のloadingは一度データを取得したらもう二度とtrueにならないことだ。 どういうことかというと、axiosの例の方は、useUserを使用しているコンポーネントがmountされるたびに、(useEffectが動くたびに) APIを必ず叩くので、しっかりとloadingが一度trueになる。 これに対して、SWRの方は二度目にマウントされた時には前回のdataが残っている(キャッシュされている)のでconst loading = !dataで定義してあるloadingはtrueにならない。 これはユーザー体験的、パフォーマンス的にはとても良い。

例えばこのようなコードがあれば、ユーザーはローディングを初回しか見ずに済む。

const { loading, user } = useUserBySWR()

if(loading) return <Loading />
return <User user={user} />

2回目以降は一瞬古いユーザーの情報が表示されて、fetchが完了次第すぐに最新のユーザー情報に切り替わる。 単にloadingをLoadingコンポーネントの出し分けのみに使うならとても良い。(古い情報が一瞬出るという点を許容できるなら)

問題のある使い方

しかし、loadingをビジネスロジックに関わるような扱い方をしたい場合は少し困る。 例えば、ページ遷移である。

// ログイン後のページの高階コンポーネント
export default function Private({ children }) {
    const { loading, notice } = useNoticeBySWR()
    useEffect(() => {
        if(loading) return
        if(notice) router.push('/notice')
    },[loading, notice])

    if(loading) return <Loading />
    return <>{children}</>
}

これはユーザーがログインした時点で未読のお知らせがあれば、お知らせページに飛んで欲しいというものである。 まぁuseEffect使うなみたいな議論はさておき。 これだけだとユーザーが以下のように動くと問題が起きる

  1. ログインする
  2. noticeが帰ってきてユーザーはお知らせページに飛ばされ、ユーザーが既読をつける
  3. ログアウトする
  4. 再度ログインする

すると、このPrivateがまたレンダリングされるわけだが、useNoticeBySWRはnoticeのキャッシュを持っている。 よって、useEffect内のif(loading) returnが効かない。 noticeがあるのでそのままユーザーはなぜか既読のお知らせに飛ばされる。 しかも、if(loading) return <Loading />も効かないので一瞬ログイン後のダッシュボード画面かなんかがちらついて、その後にお知らせに飛ばされることになる。