Skip to content

Commit c2d0c3b

Browse files
authored
Merge pull request #102 from MattCCC/timeout-abort
React Native Support & High-throughput Performance Improvements
2 parents 33108d8 + 767130b commit c2d0c3b

37 files changed

+1204
-347
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,8 @@ const api = createApiFetcher({
11491149
4. **Consider user experience** - Network revalidation happens silently in the background, providing smooth UX without loading spinners.
11501150

11511151
> ⚠️ **Browser Support**: These features work in all modern browsers that support the `focus` and `online` events. In server-side environments (Node.js), these options are safely ignored.
1152+
>
1153+
> **React Native**: Use `setEventProvider()` to enable these features. See the [React Native](#react-native) section for details.
11521154
11531155
</details>
11541156

@@ -3798,6 +3800,38 @@ For environments that do not support modern JavaScript features or APIs, you mig
37983800
- **Promise Polyfill**: For older browsers that do not support Promises. Libraries like [es6-promise](https://github.com/stefanpenner/es6-promise) can be used.
37993801
- **AbortController Polyfill**: For environments that do not support the `AbortController` API used for aborting fetch requests. You can use the [abort-controller](https://github.com/mysticatea/abort-controller) polyfill.
38003802
3803+
### React Native
3804+
3805+
`fetchff` is fully compatible with React Native. Core features like caching, retries, deduplication, and the React hook work out of the box.
3806+
3807+
To enable `refetchOnFocus` and `refetchOnReconnect`, register event providers at your app's entry point using `setEventProvider()`:
3808+
3809+
```ts
3810+
import { AppState } from 'react-native';
3811+
import NetInfo from '@react-native-community/netinfo';
3812+
import { setEventProvider } from 'fetchff';
3813+
3814+
// Refetch when app comes to foreground
3815+
setEventProvider('focus', (handler) => {
3816+
const sub = AppState.addEventListener('change', (state) => {
3817+
if (state === 'active') handler();
3818+
});
3819+
return () => sub.remove();
3820+
});
3821+
3822+
// Refetch when network reconnects
3823+
setEventProvider('online', (handler) => {
3824+
let wasConnected = true;
3825+
const unsubscribe = NetInfo.addEventListener((state) => {
3826+
if (state.isConnected && !wasConnected) handler();
3827+
wasConnected = !!state.isConnected;
3828+
});
3829+
return unsubscribe;
3830+
});
3831+
```
3832+
3833+
> **Note:** `@react-native-community/netinfo` is optional — only needed if you use `refetchOnReconnect`.
3834+
38013835
### Using `node-fetch` for Node.js < 18
38023836
38033837
If you need to support Node.js versions below 18 (not officially supported), you can use the [`node-fetch`](https://www.npmjs.com/package/node-fetch) package to polyfill the `fetch` API. Install it with:

dist/browser/index.global.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/browser/index.global.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/browser/index.mjs

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

dist/browser/index.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/node/index.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/node/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/react/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/react/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/react/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
import {useMemo,useRef,useCallback,useSyncExternalStore}from'react';import {generateCacheKey,buildConfig,getCache,getInFlightPromise,fetchf,getCachedResponse,subscribe,addTimeout,abortRequest,deleteCache}from'fetchff';var F=-1,D=2e3,m=new Map,q=e=>{e&&m.set(e,(m.get(e)||0)+1);},A=(e,t,s,o)=>{if(!e)return;let a=f(e);if(!a)return;let i=a-1;i<=0?(m.delete(e),t===F&&addTimeout("r:"+e,()=>{abortRequest(e,new DOMException("Request to "+o+" aborted","AbortError")),f(e)||deleteCache(e,true);},s!=null?s:D)):m.set(e,i);},f=e=>e&&m.get(e)||0;var Y=300,x=Object.freeze({data:null,error:null,isFetching:false,mutate:()=>Promise.resolve(null),config:{},headers:{}}),K=Object.freeze({...x,isFetching:true}),Z=[null,{},null],$=new Set(["GET","HEAD","get","head"]);function ae(e,t={}){var b,U,_;let s=useMemo(()=>e===null?null:generateCacheKey(buildConfig(e,t)),[t.cacheKey,e,t.url,t.method,t.headers,t.body,t.params,t.urlPathParams,t.apiUrl,t.baseURL,t.withCredentials,t.credentials]),o=(b=t.dedupeTime)!=null?b:D,a=t.cacheTime||F,i=(U=t.staleTime)!=null?U:Y,R=(_=t.immediate)!=null?_:$.has(t.method||"GET"),P=useRef(Z);P.current=[e,t,s];let g=useCallback(()=>{let r=getCache(s);if(t.strategy==="reject"&&s&&(!r||!r.data.data&&!r.data.error)){let h=getInFlightPromise(s,o);if(h)throw h;if(!r){let[u,c,l]=P.current;if(u)throw fetchf(u,{...c,cacheKey:l,dedupeTime:o,cacheTime:a,staleTime:i,strategy:"softFail",cacheErrors:true,_isAutoKey:!c.cacheKey})}}return r?r.data.isFetching&&!t.keepPreviousData?K:r.data:R?K:x},[s]),B=useCallback(r=>{q(s),R&&e&&s&&f(s)===1&&(getCachedResponse(s,a,t)||C(false));let u=subscribe(s,r);return ()=>{A(s,a,o,e),u();}},[s,R,e,o,a]),n=useSyncExternalStore(B,g,g),C=useCallback(async(r=true,h={})=>{let[u,c,l]=P.current;if(!u)return Promise.resolve(null);let E=!!r;if(!E&&l){let S=getCachedResponse(l,a,c);if(S)return Promise.resolve(S)}let Q=E?()=>true:c.cacheBuster;return fetchf(u,{...c,cacheKey:l,...h,dedupeTime:o,cacheTime:a,staleTime:i,cacheBuster:Q,strategy:"softFail",cacheErrors:true,_isAutoKey:!c.cacheKey})},[a,o]),T=n.data,p=!T&&!n.error,d=!!e&&(n.isFetching||p&&R),L=d&&p,w=d&&!p;return {data:T,error:n.error,config:n.config,headers:n.headers,isFirstFetch:L,isFetching:d,isLoading:d,isRefetching:w,isError:n.isError,isSuccess:n.isSuccess,mutate:n.mutate,refetch:C}}export{ae as useFetcher};//# sourceMappingURL=index.mjs.map
1+
import {useMemo,useRef,useCallback,useSyncExternalStore}from'react';import {generateCacheKey,buildConfig,getCache,getInFlightPromise,fetchf,getCachedResponse,subscribe,addTimeout,abortRequest,createAbortError,deleteCache}from'fetchff';var F=-1,y=2e3,m=new Map,S=e=>{e&&m.set(e,(m.get(e)||0)+1);},q=(e,t,r,o)=>{if(!e)return;let a=f(e);if(!a)return;let i=a-1;i<=0?(m.delete(e),t===F&&addTimeout("r:"+e,()=>{abortRequest(e,createAbortError("Request to "+o+" aborted","AbortError")),f(e)||deleteCache(e,true);},r!=null?r:y)):m.set(e,i);},f=e=>e&&m.get(e)||0;var Z=300,B=Object.freeze({data:null,error:null,isFetching:false,mutate:()=>Promise.resolve(null),config:{},headers:{}}),K=Object.freeze({...B,isFetching:true}),$=[null,{},null],k=new Set(["GET","HEAD","get","head"]);function ne(e,t={}){var b,U,_;let r=useMemo(()=>e===null?null:generateCacheKey(buildConfig(e,t)),[t.cacheKey,e,t.url,t.method,t.headers,t.body,t.params,t.urlPathParams,t.apiUrl,t.baseURL,t.withCredentials,t.credentials]),o=(b=t.dedupeTime)!=null?b:y,a=t.cacheTime||F,i=(U=t.staleTime)!=null?U:Z,R=(_=t.immediate)!=null?_:k.has(t.method||"GET"),P=useRef($);P.current=[e,t,r];let g=useCallback(()=>{let s=getCache(r);if(t.strategy==="reject"&&r&&(!s||!s.data.data&&!s.data.error)){let h=getInFlightPromise(r,o);if(h)throw h;if(!s){let[u,c,l]=P.current;if(u)throw fetchf(u,{...c,cacheKey:l,dedupeTime:o,cacheTime:a,staleTime:i,strategy:"softFail",cacheErrors:true,_isAutoKey:!c.cacheKey})}}return s?s.data.isFetching&&!t.keepPreviousData?K:s.data:R?K:B},[r]),L=useCallback(s=>{S(r),R&&e&&r&&f(r)===1&&(getCachedResponse(r,a,t)||C(false));let u=subscribe(r,s);return ()=>{q(r,a,o,e),u();}},[r,R,e,o,a]),n=useSyncExternalStore(L,g,g),C=useCallback(async(s=true,h={})=>{let[u,c,l]=P.current;if(!u)return Promise.resolve(null);let E=!!s;if(!E&&l){let A=getCachedResponse(l,a,c);if(A)return Promise.resolve(A)}let Q=E?()=>true:c.cacheBuster;return fetchf(u,{...c,cacheKey:l,...h,dedupeTime:o,cacheTime:a,staleTime:i,cacheBuster:Q,strategy:"softFail",cacheErrors:true,_isAutoKey:!c.cacheKey})},[a,o]),T=n.data,p=!T&&!n.error,d=!!e&&(n.isFetching||p&&R),x=d&&p,w=d&&!p;return {data:T,error:n.error,config:n.config,headers:n.headers,isFirstFetch:x,isFetching:d,isLoading:d,isRefetching:w,isError:n.isError,isSuccess:n.isSuccess,mutate:n.mutate,refetch:C}}export{ne as useFetcher};//# sourceMappingURL=index.mjs.map
22
//# sourceMappingURL=index.mjs.map

0 commit comments

Comments
 (0)