UNPKG

swr

Version:

React Hooks library for remote data fetching

344 lines (337 loc) 14.4 kB
import { useRef, useCallback } from 'react'; import useSWR from '../index/index.mjs'; import { withMiddleware, SWRGlobalState, cache, INFINITE_PREFIX as INFINITE_PREFIX$1, createCacheHelper, isUndefined as isUndefined$1, useIsomorphicLayoutEffect, UNDEFINED as UNDEFINED$1, serialize as serialize$1, isFunction as isFunction$1 } from '../_internal/index.mjs'; import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'; import { INFINITE_PREFIX } from '../_internal/constants.mjs'; // Shared state between server components and client components const noop = ()=>{}; // Using noop() as the undefined value as undefined can be replaced // by something else. Prettier ignore and extra parentheses are necessary here // to ensure that tsc doesn't remove the __NOINLINE__ comment. // prettier-ignore const UNDEFINED = /*#__NOINLINE__*/ noop(); const OBJECT = Object; const isUndefined = (v)=>v === UNDEFINED; const isFunction = (v)=>typeof v == 'function'; // use WeakMap to store the object->key mapping // so the objects can be garbage collected. // WeakMap uses a hashtable under the hood, so the lookup // complexity is almost O(1). const table = new WeakMap(); const isObjectType = (value, type)=>OBJECT.prototype.toString.call(value) === `[object ${type}]`; // counter of the key let counter = 0; // A stable hash implementation that supports: // - Fast and ensures unique hash properties // - Handles unserializable values // - Handles object key ordering // - Generates short results // // This is not a serialization function, and the result is not guaranteed to be // parsable. const stableHash = (arg)=>{ const type = typeof arg; const isDate = isObjectType(arg, 'Date'); const isRegex = isObjectType(arg, 'RegExp'); const isPlainObject = isObjectType(arg, 'Object'); let result; let index; if (OBJECT(arg) === arg && !isDate && !isRegex) { // Object/function, not null/date/regexp. Use WeakMap to store the id first. // If it's already hashed, directly return the result. result = table.get(arg); if (result) return result; // Store the hash first for circular reference detection before entering the // recursive `stableHash` calls. // For other objects like set and map, we use this id directly as the hash. result = ++counter + '~'; table.set(arg, result); if (Array.isArray(arg)) { // Array. result = '@'; for(index = 0; index < arg.length; index++){ result += stableHash(arg[index]) + ','; } table.set(arg, result); } if (isPlainObject) { // Object, sort keys. result = '#'; const keys = OBJECT.keys(arg).sort(); while(!isUndefined(index = keys.pop())){ if (!isUndefined(arg[index])) { result += index + ':' + stableHash(arg[index]) + ','; } } table.set(arg, result); } } else { result = isDate ? arg.toJSON() : type == 'symbol' ? arg.toString() : type == 'string' ? JSON.stringify(arg) : '' + arg; } return result; }; const serialize = (key)=>{ if (isFunction(key)) { try { key = key(); } catch (err) { // dependencies not ready key = ''; } } // Use the original key as the argument of fetcher. This can be a string or an // array of values. const args = key; // If key is not falsy, or not an empty array, hash it. key = typeof key == 'string' ? key : (Array.isArray(key) ? key.length : key) ? stableHash(key) : ''; return [ key, args ]; }; const getFirstPageKey = (getKey)=>{ return serialize(getKey ? getKey(0, null) : null)[0]; }; const unstable_serialize = (getKey)=>{ return INFINITE_PREFIX + getFirstPageKey(getKey); }; // We have to several type castings here because `useSWRInfinite` is a special // hook where `key` and return type are not like the normal `useSWR` types. const EMPTY_PROMISE = Promise.resolve(); const infinite = (useSWRNext)=>(getKey, fn, config)=>{ const didMountRef = useRef(false); const { cache: cache$1, initialSize = 1, revalidateAll = false, persistSize = false, revalidateFirstPage = true, revalidateOnMount = false, parallel = false } = config; const [, , , PRELOAD] = SWRGlobalState.get(cache); // The serialized key of the first page. This key will be used to store // metadata of this SWR infinite hook. let infiniteKey; try { infiniteKey = getFirstPageKey(getKey); if (infiniteKey) infiniteKey = INFINITE_PREFIX$1 + infiniteKey; } catch (err) { // Not ready yet. } const [get, set, subscribeCache] = createCacheHelper(cache$1, infiniteKey); const getSnapshot = useCallback(()=>{ const size = isUndefined$1(get()._l) ? initialSize : get()._l; return size; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ cache$1, infiniteKey, initialSize ]); useSyncExternalStore(useCallback((callback)=>{ if (infiniteKey) return subscribeCache(infiniteKey, ()=>{ callback(); }); return ()=>{}; }, // eslint-disable-next-line react-hooks/exhaustive-deps [ cache$1, infiniteKey ]), getSnapshot, getSnapshot); const resolvePageSize = useCallback(()=>{ const cachedPageSize = get()._l; return isUndefined$1(cachedPageSize) ? initialSize : cachedPageSize; // `cache` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps }, [ infiniteKey, initialSize ]); // keep the last page size to restore it with the persistSize option const lastPageSizeRef = useRef(resolvePageSize()); // When the page key changes, we reset the page size if it's not persisted useIsomorphicLayoutEffect(()=>{ if (!didMountRef.current) { didMountRef.current = true; return; } if (infiniteKey) { // If the key has been changed, we keep the current page size if persistSize is enabled // Otherwise, we reset the page size to cached pageSize set({ _l: persistSize ? lastPageSizeRef.current : resolvePageSize() }); } // `initialSize` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps }, [ infiniteKey, cache$1 ]); // Needs to check didMountRef during mounting, not in the fetcher const shouldRevalidateOnMount = revalidateOnMount && !didMountRef.current; // Actual SWR hook to load all pages in one fetcher. const swr = useSWRNext(infiniteKey, async (key)=>{ // get the revalidate context const forceRevalidateAll = get()._i; const shouldRevalidatePage = get()._r; set({ _r: UNDEFINED$1 }); // return an array of page data const data = []; const pageSize = resolvePageSize(); const [getCache] = createCacheHelper(cache$1, key); const cacheData = getCache().data; const revalidators = []; let previousPageData = null; for(let i = 0; i < pageSize; ++i){ const [pageKey, pageArg] = serialize$1(getKey(i, parallel ? null : previousPageData)); if (!pageKey) { break; } const [getSWRCache, setSWRCache] = createCacheHelper(cache$1, pageKey); // Get the cached page data. let pageData = getSWRCache().data; // should fetch (or revalidate) if: // - `revalidateAll` is enabled // - `mutate()` called // - the cache is missing // - it's the first page and it's not the initial render // - `revalidateOnMount` is enabled and it's on mount // - cache for that page has changed const shouldFetchPage = revalidateAll || forceRevalidateAll || isUndefined$1(pageData) || revalidateFirstPage && !i && !isUndefined$1(cacheData) || shouldRevalidateOnMount || cacheData && !isUndefined$1(cacheData[i]) && !config.compare(cacheData[i], pageData); if (fn && (typeof shouldRevalidatePage === 'function' ? shouldRevalidatePage(pageData, pageArg) : shouldFetchPage)) { const revalidate = async ()=>{ const hasPreloadedRequest = pageKey in PRELOAD; if (!hasPreloadedRequest) { pageData = await fn(pageArg); } else { const req = PRELOAD[pageKey]; // delete the preload cache key before resolving it // in case there's an error delete PRELOAD[pageKey]; // get the page data from the preload cache pageData = await req; } setSWRCache({ data: pageData, _k: pageArg }); data[i] = pageData; }; if (parallel) { revalidators.push(revalidate); } else { await revalidate(); } } else { data[i] = pageData; } if (!parallel) { previousPageData = pageData; } } // flush all revalidateions in parallel if (parallel) { await Promise.all(revalidators.map((r)=>r())); } // once we executed the data fetching based on the context, clear the context set({ _i: UNDEFINED$1 }); // return the data return data; }, config); const mutate = useCallback(// eslint-disable-next-line func-names function(data, opts) { // When passing as a boolean, it's explicitly used to disable/enable // revalidation. const options = typeof opts === 'boolean' ? { revalidate: opts } : opts || {}; // Default to true. const shouldRevalidate = options.revalidate !== false; // It is possible that the key is still falsy. if (!infiniteKey) return EMPTY_PROMISE; if (shouldRevalidate) { if (!isUndefined$1(data)) { // We only revalidate the pages that are changed set({ _i: false, _r: options.revalidate }); } else { // Calling `mutate()`, we revalidate all pages set({ _i: true, _r: options.revalidate }); } } return arguments.length ? swr.mutate(data, { ...options, revalidate: shouldRevalidate }) : swr.mutate(); }, // swr.mutate is always the same reference // eslint-disable-next-line react-hooks/exhaustive-deps [ infiniteKey, cache$1 ]); // Extend the SWR API const setSize = useCallback((arg)=>{ // It is possible that the key is still falsy. if (!infiniteKey) return EMPTY_PROMISE; const [, changeSize] = createCacheHelper(cache$1, infiniteKey); let size; if (isFunction$1(arg)) { size = arg(resolvePageSize()); } else if (typeof arg == 'number') { size = arg; } if (typeof size != 'number') return EMPTY_PROMISE; changeSize({ _l: size }); lastPageSizeRef.current = size; // Calculate the page data after the size change. const data = []; const [getInfiniteCache] = createCacheHelper(cache$1, infiniteKey); let previousPageData = null; for(let i = 0; i < size; ++i){ const [pageKey] = serialize$1(getKey(i, previousPageData)); const [getCache] = createCacheHelper(cache$1, pageKey); // Get the cached page data. const pageData = pageKey ? getCache().data : UNDEFINED$1; // Call `mutate` with infinte cache data if we can't get it from the page cache. if (isUndefined$1(pageData)) { return mutate(getInfiniteCache().data); } data.push(pageData); previousPageData = pageData; } return mutate(data); }, // exclude getKey from the dependencies, which isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps [ infiniteKey, cache$1, mutate, resolvePageSize ]); // Use getter functions to avoid unnecessary re-renders caused by triggering // all the getters of the returned swr object. return { size: resolvePageSize(), setSize, mutate, get data () { return swr.data; }, get error () { return swr.error; }, get isValidating () { return swr.isValidating; }, get isLoading () { return swr.isLoading; } }; }; const useSWRInfinite = withMiddleware(useSWR, infinite); export { useSWRInfinite as default, infinite, unstable_serialize };