Featured image of post SWRを使うときはstringをキーに渡そうねという話

SWRを使うときはstringをキーに渡そうねという話

SWRのfetcherの書き方のアンチパターン

# 結論

基本的にURLはインジェクションを防ぐためにURLオブジェクトで保持したいという気持ちがあります。しかし、URLオブジェクトで引数を渡すと、データがキャッシュされず、無限にfetchが行われてしまいます。

useSWRでつくるfetcherを作るときはURLオブジェクトを引数に取るのではなくstringを引数に取りましょう。

# はじめに

この前からSWRを使い始めました。SWRを使うにはfetcherというものを作る必要があるとのことで、私もfetcherを作ってみました。

# プロジェクトの作成

1
2
npx create-next-app
npm install swr

今回はapp routerです。

# バックエンドの構築

# だめな例

app/page.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"use client"
import useSWR from 'swr'

// DO NOT EXECUTE
const failFetcher = async (url: URL) => {
    const res = await fetch(url, {
        method: 'GET',
    })
    const data = await res.json()
    if (!res.ok) {
        throw new Error(data.message)
    }
    return data
}


const useFailFetcher = () => {
    const rawURL = new URL("http://localhost:8080")
    const url = rawURL
    const { data, error } = useSWR(url, failFetcher)
    return {
        data: data as string | undefined,
        isLoading: !data && !error,
        error: error,
    }
}


export default function Home() {
  const { data, error, isLoading } = useFailFetcher();
  console.log(data,error);
  return <>test</>;
}

このコードの実行は自己責任でお願いします。無限にリクエストが飛んで最悪ブラウザが落ちます。

バックエンド側のログの一部を抜粋するとこうなっていました。

1
2
3
4
5
6
2024/04/14 18:55:16 "GET http://localhost:8080/ HTTP/1.1" from [::1]:51354 - 200 21B in 10µs
2024/04/14 18:55:17 "GET http://localhost:8080/ HTTP/1.1" from [::1]:51354 - 200 21B in 12.461µs
2024/04/14 18:55:17 "GET http://localhost:8080/ HTTP/1.1" from [::1]:51354 - 200 21B in 11.758µs
2024/04/14 18:55:17 "GET http://localhost:8080/ HTTP/1.1" from [::1]:51354 - 200 21B in 13.048µs
2024/04/14 18:55:17 "GET http://localhost:8080/ HTTP/1.1" from [::1]:51354 - 200 21B in 16.324µs
2024/04/14 18:55:17 "GET http://localhost:8080/ HTTP/1.1" from [::1]:51354 - 200 21B in 11.658µs

数えてみたら 1 秒間にfetchが150件も飛んでいました。恐ろしい…

# 良い例

app/page.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"use client"
import useSWR from 'swr'

const useFetcher = () => {
    const rawURL = new URL("http://localhost:8080")
    const url = rawURL.toString()
    const { data, error } = useSWR(url, fetcher)
    return {
        data: data as string | undefined,
        isLoading: !data && !error,
        error: error,
    }
}
const fetcher = async (url: string) => {
    const res = await fetch(url, {
        method: 'GET',
    })
    const data = await res.json()
    if (!res.ok) {
        throw new Error(data.message)
    }
    return data
}
export default function Home() {
  const { data, error, isLoading } = useFailFetcher();
  console.log(data,error);
  return <>test</>;
}

# 原因

どうやらSWRはfetchした結果をそのpathのstringとキーバリューで保持するらしいので、URLオブジェクトにすることで一生キャッシュに失敗していたと考えられます。

ちなみに良い例の方でuseSWRに行く前にtoStringを挟んでいるのは条件付きfetchなどを行う時に予めキーとして生成しておくためです。

# 補遺

条件付きfetchを行うときの実装例をおいておきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

const authfetcher = async (url: string, jwt: string) => {
    const res = await fetch(url, {
        method: 'GET',
        headers: new Headers({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwt}` }),
    })
    const data = await res.json()
    if (!res.ok) {
        throw new Error(data.message)
    }
    return data
}
export const useAuthFetcher = (jwt: string | undefined) => {
    const rawURL = new URL("http://localhost:8080")
    rawURL.pathname = '/'
    const url = rawURL.toString()
    const { data, error } = useSWR(jwt ? [url, jwt] : null, ([url, jwt]) => authfetcher(url, jwt))
    return {
        data: data as ExampleType | undefined,
        isLoading: !data && !error,
        error: error,
    }
}
Licensed under CC BY-NC-SA 4.0
最終更新 Apr 14, 2024 00:00 UTC
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。