use-async-effekt-hooks
Version:
React hooks for async effects and memoization with proper dependency tracking and linting support
106 lines (105 loc) • 3.97 kB
JavaScript
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
import { useRef } from "react";
var sameDeps = function (a, b) {
return a.length === b.length && a.every(function (v, i) { return Object.is(v, b[i]); });
};
// Global cache for async memoization
var asyncMemoCache;
function getCache() {
if (!asyncMemoCache)
asyncMemoCache = new Map();
return asyncMemoCache;
}
function getCacheKey(factory, deps, scope) {
return JSON.stringify([factory.toString(), deps, scope || ""]);
}
/**
* @experimental This hook is experimental and should be used with caution.
*
* A hook for memoizing async computations that integrates with React Suspense.
*
* This hook allows you to perform an asynchronous operation and suspend the component
* until the operation is complete. It's useful for data fetching or any other async
* task that needs to be resolved before rendering.
*
* In SSR environments (e.g., Next.js), the hook always returns `undefined` on the
* server for prerendering. This means the suspense fallback will be displayed on
* hydration, and nothing will be displayed on the server-side render.
*
* This hook requires to be used in a client component.
*
* @param factory - The async function to execute.
* @param deps - The dependency array for the memoization.
* @param options - An optional options object.
* @param options.scope - An optional scope to isolate the cache.
* @returns The memoized value, or it suspends the component.
*
* @example
* ```tsx
* import { Suspense } from 'react';
* import { useAsyncMemoSuspense } from 'use-async-effekt-hooks';
*
* function UserProfile({ userId }) {
* const user = useAsyncMemoSuspense(async () => {
* const response = await fetch(`https://api.example.com/users/${userId}`);
* return response.json();
* }, [userId]);
*
* return (
* <div>
* <h1>{user.name}</h1>
* <p>{user.email}</p>
* </div>
* );
* }
*
* function App() {
* return (
* <Suspense fallback={<div>Loading...</div>}>
* <UserProfile userId="1" />
* </Suspense>
* );
* }
* ```
*/
export function useAsyncMemoSuspense(factory, deps, options) {
if (deps === void 0) { deps = []; }
// this is just to force the using component to be a client component
useRef(undefined);
var isClient = typeof window !== "undefined";
if (!isClient)
return undefined;
var cacheKey = getCacheKey(factory, deps, options === null || options === void 0 ? void 0 : options.scope);
var cacheEntry = getCache().get(cacheKey);
// Check if dependencies have changed or no cache entry exists
if (!cacheEntry || !sameDeps(cacheEntry.deps, deps)) {
var promise = Promise.resolve(factory());
var newCacheEntry_1 = {
status: "pending",
promise: promise,
deps: __spreadArray([], deps, true),
};
newCacheEntry_1.promise
.then(function (result) {
return Object.assign(newCacheEntry_1, { status: "success", result: result });
})
.catch(function (error) {
return Object.assign(newCacheEntry_1, { status: "error", error: error });
});
cacheEntry = newCacheEntry_1;
getCache().set(cacheKey, newCacheEntry_1);
}
if ((cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.status) === "success")
return cacheEntry.result;
if ((cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.status) === "error")
throw cacheEntry.error;
throw cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.promise;
}