Hey everyone!
I’ve been working on a lightweight React hook that mimics some of the essential features of React Query (like fetching, caching, retries, etc.) but in a more compact, easily customizable package. Below is a quick breakdown of how it works internally, referencing the relevant code sections. If you want to see the entire code, head over to the repo:
Full Source Code on GitHub.
The hook is also available on npm as api-refetch
.
Why Making My Own Hook?
While React Query and SWR are both great libraries, I wanted a more hands-on approach for a few reasons:
Lightweight Footprint
While React Query and SWR are feature-rich, they can be relatively large. Making your own hook would be ideal when package size is a big concern. This hook as meant to be installed as a dependency of another library (Intlayer). As a result, the size of the solution was an important consideration.Easy to Customize & Optimize
I needed some specific capabilities—like storing/fetching data from local storage and managing parallel requests using a straightforward approach.
By cloning the repo or copying the code directly into your project, you can strip out any unwanted features and keep only what you need. This not only reduces bundle size but also minimizes unnecessary re-renders and increase, giving you a leaner, more performant solution tailored to your specific requirements.No Required Provider
I wanted to avoid Context Provider to make the hook global, and keep it's usage as simple as possible. So I made a version of the hook based on a Zustand store (see example bellow).Learning Exercise
Building an async library from scratch is an excellent way to understand concurrency, caching, and state management internals.
In short, rolling my own hook was a chance to hone in on precisely the features I need (and skip the ones I don’t) while keeping the library small and easy to understand.
Covered Features
The React hook manage:
- Fetching & State Management: Handles loading, error, success, and fetched states.
- Caching & Storage: Optionally caches data (via React states or Zustand under the hood) and offers local storage support.
- Retries & Revalidation: Configurable retry limits and automatic revalidation intervals.
- Activation & Invalidation: Automatically activates and invalidates queries depending on other queries' or states. Example: automatically fetch some data when the user logs in. And invalidate it when the user logs out.
- Parallel Component Mount Fetching: Prevents multiple simultaneous requests for the same resource when multiple components mount at once.
How the Code Works
Below are the key points in api-refetch
and short references to the relevant parts of the code in useAsync.tsx.
1. Fetching and Handling Parallel Mounting
- Code Snippet:
// This map stores any in-progress Promise to avoid sending parallel requests
// for the same resource across multiple components.
const pendingPromises = new Map();
const fetch: T = async (...args) => {
// Check if a request with the same key + args is already running
if (pendingPromises.has(keyWithArgs)) {
return pendingPromises.get(keyWithArgs);
}
// Otherwise, store a new Promise and execute
const promise = (async () => {
setQueryState(keyWithArgs, { isLoading: true });
// ...perform fetch here...
})();
// Keep it in the map until it resolves or rejects
pendingPromises.set(keyWithArgs, promise);
return await promise;
};
-
Explanation: Here, we store any ongoing fetch in a
pendingPromises
map. When two components try to fetch the same resource simultaneously (by having the samekeyWithArgs
), the second one just reuses the ongoing request instead of making a duplicate network call.
2. Revalidation
- Code Snippet:
// Handle periodic revalidation if caching is enabled
useEffect(
() => {
if (!revalidationEnabled || revalidateTime <= 0) return; // Revalidation is disabled
if (!isEnabled || !enabled) return; // Hook is disabled
if (isLoading) return; // Fetch is already in progress
if (!isSuccess || !fetchedDateTime) return; // Should retry either of revalidate
if (!(cacheEnabled || storeEnabled)) return; // Useless to revalidate if caching is disabled
const timeout = setTimeout(() => {
fetch(...storedArgsRef.current);
}, revalidateTime);
return () => clearTimeout(timeout);
},
[
/* dependencies */
]
);
-
Explanation: Whenever you enable revalidation,
api-refetch
checks if the cached data is older than a specifiedrevalidateTime
. If it is, the data is automatically re-fetched in the background to keep your UI in sync without extra manual triggers.
3. Retry Logic
- Code Snippet:
useEffect(
() => {
const isRetryEnabled = errorCount > 0 && retryLimit > 0;
const isRetryLimitReached = errorCount > retryLimit;
if (!isEnabled || !enabled) return; // Hook is disabled
if (!isRetryEnabled) return; // Retry is disabled
if (isRetryLimitReached) return; // Retry limit has been reached
if (!(cacheEnabled || storeEnabled)) return; // Useless to retry if caching is disabled
if (isLoading) return; // Fetch is already in progress
if (isSuccess) return; // Hook has already fetched successfully
const timeout = setTimeout(() => {
fetch(...storedArgsRef.current);
}, retryTime);
return () => clearTimeout(timeout);
},
[
/* dependencies */
]
);
-
Explanation: On error, the hook counts how many failed attempts have occurred. If it’s still below the
retryLimit
, it automatically waitsretryTime
milliseconds before trying again. This process continues until data is successfully fetched or the retry limit is reached.
4. Auto-Fetch
- Code Snippet:
// Auto-fetch data on hook mount if autoFetch is true
useEffect(
() => {
if (!autoFetch) return; // Auto-fetch is disabled
if (!isEnabled || !enabled) return; // Hook is disabled
if (isFetched && !isInvalidated) return; // Hook have already fetched or invalidated
if (isLoading) return; // Fetch is already in progress
fetch(...storedArgsRef.current);
},
[
/* dependencies */
]
);
-
Explanation: With
autoFetch
set totrue
, the hook will automatically run the async function as soon as the component mounts—perfect for “fire-and-forget” scenarios where you always want the data on load.
See the Full Source on GitHub
Check out the complete code, which includes local storage logic, query invalidation, and more here:
Feel free to give it a try, report issues, or contribute if you’re interested. Any feedback is much appreciated!
Example of use
Installation
Copy the code or code the repo
Or
npm install api-refetch
Quick Example
// 1. Wrap your app in the provider (optional but recommended)
import { AsyncStateProvider, useAsync } from "api-refetch";
// import { useAsync } from "api-refetch/zustand"; // alternatively, use the zustand based hook (do not need the provider)
function App() {
return (
<AsyncStateProvider>
<UserDetails />
</AsyncStateProvider>
);
}
// 2. Define the asynchronous function that fetches your data
const fetchUserData = async (): Promise<string> => {
// Create a Promise that resolves after 1s
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: "John Doe",
});
}, 1000);
});
};
// 3. Use the hook in your component
const UserDetails = () => {
const { isLoading, data, error, revalidate } = useAsync(
"userDetails",
async () => await fetchUserData(),
{
enable: true, // enable the hook
cache: true, // cache the API call result using zustand
store: true, // store the API call result in the local storage
retryLimit: 3, // retry 3 times if the API call fails
retryTime: 10 * 1000, // wait 10 seconds before retrying
autoFetch: true, // auto fetch the API call when the component is mounted
revalidation: true, // enable revalidation
revalidateTime: 5 * 60 * 1000, // revalidate every 5 minutes
isInvalidated: false, // determine if the data is invalidated and should be refetched
invalidateQueries: ["user"], // invalidate other queries when the data is updated
updateQueries: ["user"], // set other queries data when the data is updated
onSuccess: (data) => console.log("User data fetched successfully:", data),
onError: (error) => console.error("Error fetching user data:", error),
}
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>{data?.name}</h1>
<button onClick={() => revalidate()}>Refresh</button>
</div>
);
};
That’s it! Give it a try, and let me know how it goes. Feedback, questions, or contributions are more than welcome on GitHub.
GitHub: api-refetch
Happy coding!
Top comments (0)