Vercel・SWRのコードを読んでみようの会
はじめに
SWRのコードを読んだときのメモがてら、大まかな処理の解説をしていきます。
どの処理を追うか?
基本となる、データ取得とデータ更新の処理を追います。
手元でクローンしたほうが追いやすいかもです。
注意
2021/5/23の状態のmasterの最新コードを参照しています。当たり前ですが、この解説は時とともに信憑性をなくしていきます。
データの取得
実際にuseSWRの data
プロパティがどのようなデータ源から取得されリターンされているか追います。
const { data } = useSWR('api/todos', fetcher); // dataはどこからくるのか?
まずは全ての入口となるuseSWR関数を見てみましょう。
function useSWR<Data = any, Error = any>(
...args:
| readonly [Key]
| readonly [Key, Fetcher<Data> | null]
| readonly [Key, SWRConfiguration<Data, Error> | undefined]
| readonly [
Key,
Fetcher<Data> | null,
SWRConfiguration<Data, Error> | undefined
]
): SWRResponse<Data, Error> {
const [_key, fn, config] = useArgs<Key, SWRConfiguration<Data, Error>, Data>(
args
)
...
こんな感じではじまり500行ほどuseSWRの関数の処理が続きます。
useSWRの末尾を見てみましょう。
...
const state = {
revalidate,
mutate: boundMutate
} as SWRResponse<Data, Error>
const currentStateDependencies = stateDependenciesRef.current
Object.defineProperties(state, {
data: {
get: function() {
currentStateDependencies.data = true
return data
},
enumerable: true
},
error: {
get: function() {
currentStateDependencies.error = true
return error
},
enumerable: true
},
isValidating: {
get: function() {
currentStateDependencies.isValidating = true
return isValidating
},
enumerable: true
}
})
// Display debug info in React DevTools.
useDebugValue(data)
return state
}
最後に state
というオブジェクトに色々詰めてリターンしているのがわかります。
ここの state.data
は実際にuseSWRからリターンされる値のことですね。
ではこの state.data
を中心に追っていきます。
data: {
get: function() {
currentStateDependencies.data = true
return data
},
enumerable: true
},
state.data
というものは data
というものを参照していますね。
この data
は何かというと 246行目 resolveDate
で取得しています。
// Get the current state that SWR should return.
const resolveData = () => {
const cachedData = cache.get(key)
return isUndefined(cachedData) ? config.initialData : cachedData
}
const data = resolveData()
cache.get(key)
来ましたね。SWRのキーと値はイメージ通りでJavaScriptのMapを使って実装されています。
ではこの cache
の出処を見ていきます。
function useSWR<Data = any, Error = any>(
...args:
| readonly [Key]
| readonly [Key, Fetcher<Data> | null]
| readonly [Key, SWRConfiguration<Data, Error> | undefined]
| readonly [
Key,
Fetcher<Data> | null,
SWRConfiguration<Data, Error> | undefined
]
): SWRResponse<Data, Error> {
const [_key, fn, config] = useArgs<Key, SWRConfiguration<Data, Error>, Data>(
args
)
const cache = config.cache // ココ!
...
はい、初めのところに戻ってきました。 220行目です。
cache
のもとになる config
は useArgs
から取得していますね。ではこのフックを見ましょう。
resolve-args.tsに移動します。
// Resolve arguments for SWR hooks.
// This function itself is a hook because it uses `useContext` inside.
export default function useArgs<KeyType, ConfigType, Data>(
args:
| readonly [KeyType]
| readonly [KeyType, Fetcher<Data> | null]
| readonly [KeyType, ConfigType | undefined]
| readonly [KeyType, Fetcher<Data> | null, ConfigType | undefined]
): [KeyType, Fetcher<Data> | null, (typeof defaultConfig) & ConfigType] {
const config = {
...defaultConfig,
...useContext(SWRConfigContext),
...(args.length > 2
? args[2]
: args.length === 2 && typeof args[1] === 'object'
? args[1]
: {})
} as (typeof defaultConfig) & ConfigType
// In TypeScript `args.length > 2` is not same as `args.lenth === 3`.
// We do a safe type assertion here.
const fn = (args.length > 2
? args[1]
: args.length === 2 && typeof args[1] === 'function'
? args[1]
: // Pass fn as null will disable revalidate
// https://paco.sh/blog/shared-hook-state-with-swr
args[1] === null
? args[1]
: config.fetcher) as Fetcher<Data> | null
return [args[0], fn, config]
}
config
が定義されていますね。まずは defaultConfig
に飛びます。
config.tsを見てみましょう。
const defaultConfig = {
...
cache: wrapCache(new Map()),
...
} as const
export default defaultConfig
ありました。空のMapがnewされていますね。
その後に上書きが走る useContext(SWRConfigContext)
ですね。
そうです。SWRではコンテキストを使った値管理をしています。
当然 SWRConfigContext
は cache
プロパティを持つのでグローバルに cache
プロパティにアクセスが可能になります。
データの読み込みのまとめ
- 値は全て単一のMapで管理されている
- そのMapはContextを使って管理されている
- Mapにキーアクセスして
data
プロパティがを返す
データの書き込み
cache.get
でデータを読み込めるなら cache.set
を見ればデータの書き込みが追えますね。
set
関数はいくつかの場所で呼ばれていますが、一番代表的な処理は use-swr.ts:301行目です。
処理が長いのでかいつまんで見ていきます。
...
const revalidate = useCallback(async (revalidateOpts: RevalidatorOptions = {}): Promise<boolean> => {
...
// Try構文を使用している。
try {
...
if (fnArgs !== null) { // 346行目
CONCURRENT_PROMISES[key] = fn(...fnArgs) // fn関数はfetcher関数のこと。これを評価することで非同期の値が取れる。
} else {
CONCURRENT_PROMISES[key] = fn(key)
}
CONCURRENT_PROMISES_TS[key] = startAt = now()
// 非同期のfethcerをawaitすることで値を取得する
newData = await CONCURRENT_PROMISES[key] // 354行目
...
// 取得したデータをcacheにセット
cache.set(key, newData) // 423行目
// 値を他のフックにブロードキャストする
// also update other hooks
broadcastState(cache, key, newData, newState.error, false) // 431行目
...
}
...
}
細かい説明は省きますが、fetcher関数を評価してそれをcacheにセットする。そのあとブロードキャストをする流れが見えます。
また、この revalidate
関数はマウント時に呼ばれていて use-swr.ts:495行目でそれを確認できます。
...
// After mounted or key changed.
useIsomorphicLayoutEffect(() => {
if (!key) return UNDEFINED
// Not the inital render.
const keyChanged = initialMountedRef.current
const softRevalidate = () => revalidate({ dedupe: true }) //ここで呼ばれる
...
この useIsomorphicLayoutEffect
は下記のような実装になっています。
export const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect
シンプルな useEffect
(サーバー時) or useLayoutEffect
(クライアント時) ですね。
データの書き込みのまとめ
-
revalidate
関数で主に書き込みがされている。 -
fetcher
関数をawaitで評価し、cache
にセットしている。 - その後
broadcastState
関数で値をブロードキャストしている。 -
revalidate
関数はマウント時に呼ばれている
おわりに
OSSのコードリーディングは楽しいですね。かなり細かい説明は省いていますが、大まかな流れを理解できたら幸いです。
僕が働いているログラスではフロントエンドエンジニア絶賛募集中ですのでもし興味あればぜひ!
Discussion