@mustafaaksoy41/react-native-offline-queue 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,21 +3,22 @@
3
3
  # 📡 react-native-offline-queue
4
4
 
5
5
  **A lightweight, high-performance offline queue and sync manager for React Native.**
6
- Queue operations when offline, sync automatically or manually when connectivity returns.
6
+ Queue operations when offline, sync automatically or manually when connectivity returns. Works great with React Query (TanStack Query).
7
7
 
8
8
  <br />
9
9
 
10
10
  <!-- Package Info -->
11
11
  [![npm version](https://img.shields.io/npm/v/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@mustafaaksoy41/react-native-offline-queue)
12
12
  [![npm downloads](https://img.shields.io/npm/dm/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@mustafaaksoy41/react-native-offline-queue)
13
- [![license](https://img.shields.io/npm/l/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=opensourceinitiative&logoColor=white&color=3DA639)](https://github.com/mustafaaksoy41/react-native-offline-queue/blob/main/LICENSE)
14
- [![bundle size](https://img.shields.io/bundlephobia/minzip/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=webpack&logoColor=white&color=8DD6F9&label=size)](https://bundlephobia.com/package/@mustafaaksoy41/react-native-offline-queue)
13
+ [![license](https://img.shields.io/npm/l/@mustafaaksoy41/react-native-offline-queue?style=for-the-badge&logo=opensourceinitiative&logoColor=white&color=3DA639)](https://github.com/messivite/react-native-offline-queue/blob/main/LICENSE)
14
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/%40mustafaaksoy41%2Freact-native-offline-queue?style=for-the-badge&logo=webpack&logoColor=white&color=8DD6F9&label=size)](https://bundlephobia.com/package/@mustafaaksoy41/react-native-offline-queue)
15
15
 
16
16
  <!-- Platform & Language -->
17
17
  [![Platform - Android](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://reactnative.dev/)
18
18
  [![Platform - iOS](https://img.shields.io/badge/iOS-000000?style=for-the-badge&logo=apple&logoColor=white)](https://reactnative.dev/)
19
19
  [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
20
20
  [![React Native](https://img.shields.io/badge/React_Native-61DAFB?style=for-the-badge&logo=react&logoColor=black)](https://reactnative.dev/)
21
+ [![React Query](https://img.shields.io/badge/React_Query-FF4154?style=for-the-badge&logo=tanstackquery&logoColor=white)](https://tanstack.com/query/latest)
21
22
 
22
23
  <!-- Supported Storage Adapters -->
23
24
  [![MMKV](https://img.shields.io/badge/MMKV-FF6C37?style=for-the-badge&logo=firebase&logoColor=white)](https://github.com/mrousavy/react-native-mmkv)
@@ -185,6 +186,46 @@ function LikeButton({ postId }) {
185
186
  - **Offline**: The action is saved to the queue, and `onOptimisticSuccess` fires so the UI updates instantly.
186
187
  - **When connectivity returns**: Queued actions are synced — per-action handler first, then `onSyncAction` as fallback.
187
188
 
189
+ ### How API requests work (real URLs)
190
+
191
+ The `handler` is where you make the **actual API call** — `fetch`, axios, or your API client. When the user presses a button:
192
+
193
+ 1. **Online**: `handler(payload)` runs immediately → your `fetch('https://api.myapp.com/...')` fires right away.
194
+ 2. **Offline**: `{ actionName, payload }` is stored in the queue. When connectivity returns, the queue flushes and `handler(payload)` runs for each item → your real API requests are sent.
195
+
196
+ ```tsx
197
+ function CreatePostScreen() {
198
+ const { mutateOffline } = useOfflineMutation('CREATE_POST', {
199
+ handler: async (payload) => {
200
+ // Your real API URL — runs immediately when online, or after queue flushes when offline
201
+ const res = await fetch('https://api.myapp.com/v1/posts', {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ 'Authorization': `Bearer ${await getAuthToken()}`,
206
+ },
207
+ body: JSON.stringify(payload),
208
+ });
209
+ if (!res.ok) throw new Error(await res.text());
210
+ },
211
+ onOptimisticSuccess: () => navigation.goBack(),
212
+ });
213
+
214
+ return (
215
+ <Button
216
+ title="Submit"
217
+ onPress={() => mutateOffline({ title, body })}
218
+ />
219
+ );
220
+ }
221
+ ```
222
+
223
+ | User action | Network | What happens |
224
+ |-------------|---------|--------------|
225
+ | Button press | Online | `handler` runs → `fetch` fires → API receives request |
226
+ | Button press | Offline | Action queued, `onOptimisticSuccess` fires, UI updates |
227
+ | Connectivity restores | — | Queue flushes → each `handler` runs → real API calls sent |
228
+
188
229
  ### Full Example
189
230
 
190
231
  Here's what a real app looks like with multiple offline-capable actions. Each component owns its own API logic — no central switch-case needed.
@@ -264,6 +305,86 @@ function MessageBubble({ chatId }) {
264
305
 
265
306
  Each `handler` is self-contained: when the user goes offline, actions are queued with their `actionName`. When connectivity returns, the queue flushes and each action runs through its registered handler automatically.
266
307
 
308
+ ### Using with React Query (TanStack Query)
309
+
310
+ Use your **existing `useMutation` hook** — the handler calls `mutateAsync`. No duplicate fetch, no extra API layer. Your mutation does everything (POST, cache invalidation, etc.).
311
+
312
+ **Pattern — Handler uses `mutateAsync` from `useMutation`**
313
+
314
+ ```tsx
315
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
316
+ import { useOfflineMutation } from '@mustafaaksoy41/react-native-offline-queue';
317
+
318
+ function CreatePostForm() {
319
+ const queryClient = useQueryClient();
320
+
321
+ // Your existing React Query mutation — mutationFn, onSuccess, retry, etc.
322
+ const { mutateAsync } = useMutation({
323
+ mutationFn: (payload: { title: string; body: string }) =>
324
+ fetch('https://api.myapp.com/posts', {
325
+ method: 'POST',
326
+ headers: { 'Content-Type': 'application/json' },
327
+ body: JSON.stringify(payload),
328
+ }).then((r) => r.json()),
329
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
330
+ });
331
+
332
+ // Handler = your mutation. Online: runs immediately. Offline: queued, runs when back online.
333
+ const { mutateOffline } = useOfflineMutation('CREATE_POST', {
334
+ handler: async (payload) => {
335
+ await mutateAsync(payload);
336
+ },
337
+ onOptimisticSuccess: (payload) => {
338
+ queryClient.setQueryData(['posts'], (old: any) =>
339
+ old ? [...old, { ...payload, id: 'temp', pending: true }] : [payload]
340
+ );
341
+ },
342
+ });
343
+
344
+ return (
345
+ <Button
346
+ title="Post"
347
+ onPress={() => mutateOffline({ title, body })}
348
+ />
349
+ );
350
+ }
351
+ ```
352
+
353
+ **With custom hooks / API layer**
354
+
355
+ If your mutations live in custom hooks:
356
+
357
+ ```tsx
358
+ // hooks/useCreatePost.ts
359
+ export function useCreatePost() {
360
+ const queryClient = useQueryClient();
361
+ return useMutation({
362
+ mutationFn: api.createPost, // your axios/fetch wrapper
363
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
364
+ });
365
+ }
366
+
367
+ // CreatePostForm.tsx
368
+ function CreatePostForm() {
369
+ const { mutateAsync } = useCreatePost();
370
+
371
+ const { mutateOffline } = useOfflineMutation('CREATE_POST', {
372
+ handler: async (payload) => await mutateAsync(payload),
373
+ onOptimisticSuccess: (payload) => { /* ... */ },
374
+ });
375
+
376
+ return <Button onPress={() => mutateOffline({ title, body })} />;
377
+ }
378
+ ```
379
+
380
+ **Summary**
381
+
382
+ | Handler does | Your mutation does |
383
+ |--------------|--------------------|
384
+ | Calls `mutateAsync(payload)` | POST/GET, retry, cache invalidation, error handling |
385
+
386
+ No duplicate logic. Same mutation for online and offline sync.
387
+
267
388
  ## Configuration
268
389
 
269
390
  ```tsx
@@ -354,31 +475,94 @@ Omit `onOnlineRestore` entirely. Nothing happens — you handle sync manually th
354
475
 
355
476
  ### `useOfflineMutation(actionName, options?)`
356
477
 
357
- Queue-aware mutation hook. Calls the handler directly when online, queues when offline.
478
+ Queue-aware mutation hook with built-in state tracking. Calls the handler directly when online, queues when offline.
358
479
 
359
- ```tsx
360
- const { mutateOffline } = useOfflineMutation('CREATE_POST', {
361
- handler: async (payload) => {
362
- // Your API call — runs directly when online, or during sync when offline
363
- await api.createPost(payload);
364
- },
365
- onOptimisticSuccess: (payload) => {
366
- // Runs immediately — update your local state here
367
- },
368
- onError: (error, payload) => {
369
- // Runs if the direct API call fails while online
370
- },
371
- });
480
+ **Returns:**
372
481
 
373
- mutateOffline({ title: 'Hello', body: 'World' });
482
+ ```tsx
483
+ const {
484
+ mutateOffline, // (payload) => Promise<void>
485
+ status, // 'idle' | 'loading' | 'success' | 'error' | 'queued'
486
+ isIdle, // true before any mutation
487
+ isLoading, // true while the handler is running (online only)
488
+ isSuccess, // true after a successful direct call
489
+ isError, // true if the direct call threw (action still queued as fallback)
490
+ isQueued, // true when the action was added to the offline queue
491
+ error, // Error | null
492
+ reset, // () => void — reset status back to idle
493
+ } = useOfflineMutation('ACTION_NAME', options);
374
494
  ```
375
495
 
376
496
  | Option | Type | Description |
377
497
  |--------|------|-------------|
378
- | `handler` | `(payload) => Promise<void>` | API call for this specific action. Registered automatically. |
379
- | `onOptimisticSuccess` | `(payload) => void` | Fires immediately for instant UI updates |
498
+ | `handler` | `(payload) => Promise<void>` | API call for this action. Registered automatically, used during sync. |
499
+ | `onOptimisticSuccess` | `(payload) => void` | Fires immediately update your local state here |
500
+ | `onSuccess` | `(payload) => void` | Fires only after a successful direct call (online) |
380
501
  | `onError` | `(error, payload) => void` | Fires if the direct call fails while online |
381
502
 
503
+ **With `fetch`:**
504
+
505
+ ```tsx
506
+ function LikeButton({ postId }) {
507
+ const { mutateOffline, isLoading, isQueued } = useOfflineMutation('LIKE_POST', {
508
+ handler: async (payload) => {
509
+ await fetch('/api/likes', {
510
+ method: 'POST',
511
+ body: JSON.stringify(payload),
512
+ });
513
+ },
514
+ onOptimisticSuccess: () => setLiked(true),
515
+ });
516
+
517
+ return (
518
+ <Button
519
+ title={isLoading ? '⏳' : isQueued ? '📡 Queued' : '❤️ Like'}
520
+ onPress={() => mutateOffline({ postId })}
521
+ disabled={isLoading}
522
+ />
523
+ );
524
+ }
525
+ ```
526
+
527
+ **With React Query:**
528
+
529
+ ```tsx
530
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
531
+
532
+ function LikeButton({ postId }) {
533
+ const queryClient = useQueryClient();
534
+
535
+ const { mutateAsync } = useMutation({
536
+ mutationFn: (payload) => api.likePost(payload),
537
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
538
+ });
539
+
540
+ const { mutateOffline, isQueued } = useOfflineMutation('LIKE_POST', {
541
+ handler: async (payload) => await mutateAsync(payload),
542
+ onOptimisticSuccess: () => {
543
+ queryClient.setQueryData(['posts', postId], (old) => ({
544
+ ...old, liked: true,
545
+ }));
546
+ },
547
+ });
548
+
549
+ return (
550
+ <Button
551
+ title={isQueued ? '📡 Queued' : '❤️ Like'}
552
+ onPress={() => mutateOffline({ postId })}
553
+ />
554
+ );
555
+ }
556
+ ```
557
+
558
+ **State flow:**
559
+
560
+ | Scenario | `status` flow |
561
+ |----------|---------------|
562
+ | Online + success | `idle` → `loading` → `success` |
563
+ | Online + API fails | `idle` → `loading` → `queued` (fallback) |
564
+ | Offline | `idle` → `queued` |
565
+
382
566
  ### `useOfflineQueue()`
383
567
 
384
568
  Access the live queue state. Uses `useSyncExternalStore` under the hood — only re-renders when the queue actually changes.
@@ -13,6 +13,8 @@ function useOfflineMutation(actionName, options) {
13
13
  } = (0, _OfflineProvider.useNetworkStatus)();
14
14
  const handlerRef = (0, _react.useRef)(options?.handler);
15
15
  handlerRef.current = options?.handler;
16
+ const [status, setStatus] = (0, _react.useState)('idle');
17
+ const [error, setError] = (0, _react.useState)(null);
16
18
 
17
19
  // Register per-action handler (persists even after unmount)
18
20
  (0, _react.useEffect)(() => {
@@ -20,7 +22,11 @@ function useOfflineMutation(actionName, options) {
20
22
  _OfflineManager.OfflineManager.registerHandler(actionName, payload => handlerRef.current(payload));
21
23
  }
22
24
  }, [actionName]);
23
- const mutateOffline = async payload => {
25
+ const reset = (0, _react.useCallback)(() => {
26
+ setStatus('idle');
27
+ setError(null);
28
+ }, []);
29
+ const mutateOffline = (0, _react.useCallback)(async payload => {
24
30
  // Resolve which handler to use: per-action handler > global onSyncAction
25
31
  const handler = handlerRef.current || _OfflineManager.OfflineManager.getHandler(actionName);
26
32
  const globalHandler = _OfflineManager.OfflineManager.onSyncAction;
@@ -28,6 +34,8 @@ function useOfflineMutation(actionName, options) {
28
34
  if (isOnline && hasHandler) {
29
35
  // ── ONLINE: Execute directly, skip the queue ──
30
36
  if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (direct)`);
37
+ setStatus('loading');
38
+ setError(null);
31
39
  try {
32
40
  if (handler) {
33
41
  await handler(payload);
@@ -40,22 +48,37 @@ function useOfflineMutation(actionName, options) {
40
48
  retryCount: 0
41
49
  });
42
50
  }
51
+ setStatus('success');
43
52
  options?.onOptimisticSuccess?.(payload);
44
- } catch (error) {
45
- console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, error);
53
+ options?.onSuccess?.(payload);
54
+ } catch (err) {
55
+ console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, err);
56
+ // API failed even though online → fallback to queue
46
57
  await _OfflineManager.OfflineManager.push(actionName, payload);
58
+ setStatus('queued');
59
+ setError(err);
47
60
  options?.onOptimisticSuccess?.(payload);
48
- options?.onError?.(error, payload);
61
+ options?.onError?.(err, payload);
49
62
  }
50
63
  } else {
51
64
  // ── OFFLINE: Add to queue + optimistic update ──
52
65
  if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (queued)`);
53
66
  await _OfflineManager.OfflineManager.push(actionName, payload);
67
+ setStatus('queued');
68
+ setError(null);
54
69
  options?.onOptimisticSuccess?.(payload);
55
70
  }
56
- };
71
+ }, [actionName, isOnline, options]);
57
72
  return {
58
- mutateOffline
73
+ mutateOffline,
74
+ status,
75
+ isIdle: status === 'idle',
76
+ isLoading: status === 'loading',
77
+ isSuccess: status === 'success',
78
+ isError: status === 'error',
79
+ isQueued: status === 'queued',
80
+ error,
81
+ reset
59
82
  };
60
83
  }
61
84
  //# sourceMappingURL=useOfflineMutation.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_react","require","_OfflineManager","_OfflineProvider","useOfflineMutation","actionName","options","isOnline","useNetworkStatus","handlerRef","useRef","handler","current","useEffect","OfflineManager","registerHandler","payload","mutateOffline","getHandler","globalHandler","onSyncAction","hasHandler","__DEV__","console","log","id","createdAt","Date","now","retryCount","onOptimisticSuccess","error","warn","push","onError"],"sourceRoot":"../../../src","sources":["hooks/useOfflineMutation.ts"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,eAAA,GAAAD,OAAA;AACA,IAAAE,gBAAA,GAAAF,OAAA;AAEO,SAASG,kBAAkBA,CAChCC,UAAkB,EAClBC,OAIC,EACD;EACA,MAAM;IAAEC;EAAS,CAAC,GAAG,IAAAC,iCAAgB,EAAC,CAAC;EACvC,MAAMC,UAAU,GAAG,IAAAC,aAAM,EAACJ,OAAO,EAAEK,OAAO,CAAC;EAC3CF,UAAU,CAACG,OAAO,GAAGN,OAAO,EAAEK,OAAO;;EAErC;EACA,IAAAE,gBAAS,EAAC,MAAM;IACd,IAAIJ,UAAU,CAACG,OAAO,EAAE;MACtBE,8BAAc,CAACC,eAAe,CAACV,UAAU,EAAGW,OAAY,IACtDP,UAAU,CAACG,OAAO,CAAEI,OAAO,CAC7B,CAAC;IACH;EACF,CAAC,EAAE,CAACX,UAAU,CAAC,CAAC;EAEhB,MAAMY,aAAa,GAAG,MAAOD,OAAiB,IAAK;IACjD;IACA,MAAML,OAAO,GAAGF,UAAU,CAACG,OAAO,IAAIE,8BAAc,CAACI,UAAU,CAACb,UAAU,CAAC;IAC3E,MAAMc,aAAa,GAAGL,8BAAc,CAACM,YAAY;IACjD,MAAMC,UAAU,GAAGV,OAAO,IAAIQ,aAAa;IAE3C,IAAIZ,QAAQ,IAAIc,UAAU,EAAE;MAC1B;MACA,IAAIC,OAAO,EAAEC,OAAO,CAACC,GAAG,CAAC,0BAA0BnB,UAAU,WAAW,CAAC;MACzE,IAAI;QACF,IAAIM,OAAO,EAAE;UACX,MAAMA,OAAO,CAACK,OAAO,CAAC;QACxB,CAAC,MAAM,IAAIG,aAAa,EAAE;UACxB,MAAMA,aAAa,CAAC;YAClBM,EAAE,EAAE,EAAE;YACNpB,UAAU;YACVW,OAAO;YACPU,SAAS,EAAEC,IAAI,CAACC,GAAG,CAAC,CAAC;YACrBC,UAAU,EAAE;UACd,CAAC,CAAC;QACJ;QACAvB,OAAO,EAAEwB,mBAAmB,GAAGd,OAAO,CAAC;MACzC,CAAC,CAAC,OAAOe,KAAU,EAAE;QACnBR,OAAO,CAACS,IAAI,CAAC,0BAA0B3B,UAAU,gCAAgC,EAAE0B,KAAK,CAAC;QACzF,MAAMjB,8BAAc,CAACmB,IAAI,CAAC5B,UAAU,EAAEW,OAAO,CAAC;QAC9CV,OAAO,EAAEwB,mBAAmB,GAAGd,OAAO,CAAC;QACvCV,OAAO,EAAE4B,OAAO,GAAGH,KAAK,EAAEf,OAAO,CAAC;MACpC;IACF,CAAC,MAAM;MACL;MACA,IAAIM,OAAO,EAAEC,OAAO,CAACC,GAAG,CAAC,0BAA0BnB,UAAU,WAAW,CAAC;MACzE,MAAMS,8BAAc,CAACmB,IAAI,CAAC5B,UAAU,EAAEW,OAAO,CAAC;MAC9CV,OAAO,EAAEwB,mBAAmB,GAAGd,OAAO,CAAC;IACzC;EACF,CAAC;EAED,OAAO;IAAEC;EAAc,CAAC;AAC1B","ignoreList":[]}
1
+ {"version":3,"names":["_react","require","_OfflineManager","_OfflineProvider","useOfflineMutation","actionName","options","isOnline","useNetworkStatus","handlerRef","useRef","handler","current","status","setStatus","useState","error","setError","useEffect","OfflineManager","registerHandler","payload","reset","useCallback","mutateOffline","getHandler","globalHandler","onSyncAction","hasHandler","__DEV__","console","log","id","createdAt","Date","now","retryCount","onOptimisticSuccess","onSuccess","err","warn","push","onError","isIdle","isLoading","isSuccess","isError","isQueued"],"sourceRoot":"../../../src","sources":["hooks/useOfflineMutation.ts"],"mappings":"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","ignoreList":[]}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import { useEffect, useRef } from 'react';
3
+ import { useEffect, useRef, useState, useCallback } from 'react';
4
4
  import { OfflineManager } from '../core/OfflineManager';
5
5
  import { useNetworkStatus } from '../components/OfflineProvider';
6
6
  export function useOfflineMutation(actionName, options) {
@@ -9,6 +9,8 @@ export function useOfflineMutation(actionName, options) {
9
9
  } = useNetworkStatus();
10
10
  const handlerRef = useRef(options?.handler);
11
11
  handlerRef.current = options?.handler;
12
+ const [status, setStatus] = useState('idle');
13
+ const [error, setError] = useState(null);
12
14
 
13
15
  // Register per-action handler (persists even after unmount)
14
16
  useEffect(() => {
@@ -16,7 +18,11 @@ export function useOfflineMutation(actionName, options) {
16
18
  OfflineManager.registerHandler(actionName, payload => handlerRef.current(payload));
17
19
  }
18
20
  }, [actionName]);
19
- const mutateOffline = async payload => {
21
+ const reset = useCallback(() => {
22
+ setStatus('idle');
23
+ setError(null);
24
+ }, []);
25
+ const mutateOffline = useCallback(async payload => {
20
26
  // Resolve which handler to use: per-action handler > global onSyncAction
21
27
  const handler = handlerRef.current || OfflineManager.getHandler(actionName);
22
28
  const globalHandler = OfflineManager.onSyncAction;
@@ -24,6 +30,8 @@ export function useOfflineMutation(actionName, options) {
24
30
  if (isOnline && hasHandler) {
25
31
  // ── ONLINE: Execute directly, skip the queue ──
26
32
  if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (direct)`);
33
+ setStatus('loading');
34
+ setError(null);
27
35
  try {
28
36
  if (handler) {
29
37
  await handler(payload);
@@ -36,22 +44,37 @@ export function useOfflineMutation(actionName, options) {
36
44
  retryCount: 0
37
45
  });
38
46
  }
47
+ setStatus('success');
39
48
  options?.onOptimisticSuccess?.(payload);
40
- } catch (error) {
41
- console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, error);
49
+ options?.onSuccess?.(payload);
50
+ } catch (err) {
51
+ console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, err);
52
+ // API failed even though online → fallback to queue
42
53
  await OfflineManager.push(actionName, payload);
54
+ setStatus('queued');
55
+ setError(err);
43
56
  options?.onOptimisticSuccess?.(payload);
44
- options?.onError?.(error, payload);
57
+ options?.onError?.(err, payload);
45
58
  }
46
59
  } else {
47
60
  // ── OFFLINE: Add to queue + optimistic update ──
48
61
  if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (queued)`);
49
62
  await OfflineManager.push(actionName, payload);
63
+ setStatus('queued');
64
+ setError(null);
50
65
  options?.onOptimisticSuccess?.(payload);
51
66
  }
52
- };
67
+ }, [actionName, isOnline, options]);
53
68
  return {
54
- mutateOffline
69
+ mutateOffline,
70
+ status,
71
+ isIdle: status === 'idle',
72
+ isLoading: status === 'loading',
73
+ isSuccess: status === 'success',
74
+ isError: status === 'error',
75
+ isQueued: status === 'queued',
76
+ error,
77
+ reset
55
78
  };
56
79
  }
57
80
  //# sourceMappingURL=useOfflineMutation.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["useEffect","useRef","OfflineManager","useNetworkStatus","useOfflineMutation","actionName","options","isOnline","handlerRef","handler","current","registerHandler","payload","mutateOffline","getHandler","globalHandler","onSyncAction","hasHandler","__DEV__","console","log","id","createdAt","Date","now","retryCount","onOptimisticSuccess","error","warn","push","onError"],"sourceRoot":"../../../src","sources":["hooks/useOfflineMutation.ts"],"mappings":";;AAAA,SAASA,SAAS,EAAEC,MAAM,QAAQ,OAAO;AACzC,SAASC,cAAc,QAAQ,wBAAwB;AACvD,SAASC,gBAAgB,QAAQ,+BAA+B;AAEhE,OAAO,SAASC,kBAAkBA,CAChCC,UAAkB,EAClBC,OAIC,EACD;EACA,MAAM;IAAEC;EAAS,CAAC,GAAGJ,gBAAgB,CAAC,CAAC;EACvC,MAAMK,UAAU,GAAGP,MAAM,CAACK,OAAO,EAAEG,OAAO,CAAC;EAC3CD,UAAU,CAACE,OAAO,GAAGJ,OAAO,EAAEG,OAAO;;EAErC;EACAT,SAAS,CAAC,MAAM;IACd,IAAIQ,UAAU,CAACE,OAAO,EAAE;MACtBR,cAAc,CAACS,eAAe,CAACN,UAAU,EAAGO,OAAY,IACtDJ,UAAU,CAACE,OAAO,CAAEE,OAAO,CAC7B,CAAC;IACH;EACF,CAAC,EAAE,CAACP,UAAU,CAAC,CAAC;EAEhB,MAAMQ,aAAa,GAAG,MAAOD,OAAiB,IAAK;IACjD;IACA,MAAMH,OAAO,GAAGD,UAAU,CAACE,OAAO,IAAIR,cAAc,CAACY,UAAU,CAACT,UAAU,CAAC;IAC3E,MAAMU,aAAa,GAAGb,cAAc,CAACc,YAAY;IACjD,MAAMC,UAAU,GAAGR,OAAO,IAAIM,aAAa;IAE3C,IAAIR,QAAQ,IAAIU,UAAU,EAAE;MAC1B;MACA,IAAIC,OAAO,EAAEC,OAAO,CAACC,GAAG,CAAC,0BAA0Bf,UAAU,WAAW,CAAC;MACzE,IAAI;QACF,IAAII,OAAO,EAAE;UACX,MAAMA,OAAO,CAACG,OAAO,CAAC;QACxB,CAAC,MAAM,IAAIG,aAAa,EAAE;UACxB,MAAMA,aAAa,CAAC;YAClBM,EAAE,EAAE,EAAE;YACNhB,UAAU;YACVO,OAAO;YACPU,SAAS,EAAEC,IAAI,CAACC,GAAG,CAAC,CAAC;YACrBC,UAAU,EAAE;UACd,CAAC,CAAC;QACJ;QACAnB,OAAO,EAAEoB,mBAAmB,GAAGd,OAAO,CAAC;MACzC,CAAC,CAAC,OAAOe,KAAU,EAAE;QACnBR,OAAO,CAACS,IAAI,CAAC,0BAA0BvB,UAAU,gCAAgC,EAAEsB,KAAK,CAAC;QACzF,MAAMzB,cAAc,CAAC2B,IAAI,CAACxB,UAAU,EAAEO,OAAO,CAAC;QAC9CN,OAAO,EAAEoB,mBAAmB,GAAGd,OAAO,CAAC;QACvCN,OAAO,EAAEwB,OAAO,GAAGH,KAAK,EAAEf,OAAO,CAAC;MACpC;IACF,CAAC,MAAM;MACL;MACA,IAAIM,OAAO,EAAEC,OAAO,CAACC,GAAG,CAAC,0BAA0Bf,UAAU,WAAW,CAAC;MACzE,MAAMH,cAAc,CAAC2B,IAAI,CAACxB,UAAU,EAAEO,OAAO,CAAC;MAC9CN,OAAO,EAAEoB,mBAAmB,GAAGd,OAAO,CAAC;IACzC;EACF,CAAC;EAED,OAAO;IAAEC;EAAc,CAAC;AAC1B","ignoreList":[]}
1
+ {"version":3,"names":["useEffect","useRef","useState","useCallback","OfflineManager","useNetworkStatus","useOfflineMutation","actionName","options","isOnline","handlerRef","handler","current","status","setStatus","error","setError","registerHandler","payload","reset","mutateOffline","getHandler","globalHandler","onSyncAction","hasHandler","__DEV__","console","log","id","createdAt","Date","now","retryCount","onOptimisticSuccess","onSuccess","err","warn","push","onError","isIdle","isLoading","isSuccess","isError","isQueued"],"sourceRoot":"../../../src","sources":["hooks/useOfflineMutation.ts"],"mappings":"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","ignoreList":[]}
@@ -1,8 +1,19 @@
1
+ export type MutationStatus = 'idle' | 'loading' | 'success' | 'error' | 'queued';
2
+ export interface OfflineMutationResult<TPayload> {
3
+ mutateOffline: (payload: TPayload) => Promise<void>;
4
+ status: MutationStatus;
5
+ isIdle: boolean;
6
+ isLoading: boolean;
7
+ isSuccess: boolean;
8
+ isError: boolean;
9
+ isQueued: boolean;
10
+ error: Error | null;
11
+ reset: () => void;
12
+ }
1
13
  export declare function useOfflineMutation<TPayload>(actionName: string, options?: {
2
14
  handler?: (payload: TPayload) => Promise<void>;
3
15
  onOptimisticSuccess?: (payload: TPayload) => void;
4
16
  onError?: (error: Error, payload: TPayload) => void;
5
- }): {
6
- mutateOffline: (payload: TPayload) => Promise<void>;
7
- };
17
+ onSuccess?: (payload: TPayload) => void;
18
+ }): OfflineMutationResult<TPayload>;
8
19
  //# sourceMappingURL=useOfflineMutation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useOfflineMutation.d.ts","sourceRoot":"","sources":["../../../src/hooks/useOfflineMutation.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,QAAQ,EACzC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;CACrD;6BAeqC,QAAQ;EAqC/C"}
1
+ {"version":3,"file":"useOfflineMutation.d.ts","sourceRoot":"","sources":["../../../src/hooks/useOfflineMutation.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEjF,MAAM,WAAW,qBAAqB,CAAC,QAAQ;IAC7C,aAAa,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EACzC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;CACzC,GACA,qBAAqB,CAAC,QAAQ,CAAC,CA8EjC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mustafaaksoy41/react-native-offline-queue",
3
- "version": "0.1.2",
4
- "description": "A flexible, high-performance offline queue and synchronizer for React Native.",
3
+ "version": "0.1.4",
4
+ "description": "A flexible, high-performance offline queue and synchronizer for React Native. Works great with React Query (TanStack Query).",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
7
7
  "types": "lib/typescript/index.d.ts",
@@ -23,10 +23,20 @@
23
23
  "offline",
24
24
  "queue",
25
25
  "sync",
26
- "hook"
26
+ "hook",
27
+ "react-query",
28
+ "tanstack-query"
27
29
  ],
28
30
  "author": "Mustafa Aksoy",
29
31
  "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/messivite/react-native-offline-queue.git"
35
+ },
36
+ "homepage": "https://github.com/messivite/react-native-offline-queue#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/messivite/react-native-offline-queue/issues"
39
+ },
30
40
  "peerDependencies": {
31
41
  "@react-native-async-storage/async-storage": "*",
32
42
  "@react-native-community/netinfo": "*",
@@ -1,19 +1,37 @@
1
- import { useEffect, useRef } from 'react';
1
+ import { useEffect, useRef, useState, useCallback } from 'react';
2
2
  import { OfflineManager } from '../core/OfflineManager';
3
3
  import { useNetworkStatus } from '../components/OfflineProvider';
4
4
 
5
+ export type MutationStatus = 'idle' | 'loading' | 'success' | 'error' | 'queued';
6
+
7
+ export interface OfflineMutationResult<TPayload> {
8
+ mutateOffline: (payload: TPayload) => Promise<void>;
9
+ status: MutationStatus;
10
+ isIdle: boolean;
11
+ isLoading: boolean;
12
+ isSuccess: boolean;
13
+ isError: boolean;
14
+ isQueued: boolean;
15
+ error: Error | null;
16
+ reset: () => void;
17
+ }
18
+
5
19
  export function useOfflineMutation<TPayload>(
6
20
  actionName: string,
7
21
  options?: {
8
22
  handler?: (payload: TPayload) => Promise<void>;
9
23
  onOptimisticSuccess?: (payload: TPayload) => void;
10
24
  onError?: (error: Error, payload: TPayload) => void;
25
+ onSuccess?: (payload: TPayload) => void;
11
26
  }
12
- ) {
27
+ ): OfflineMutationResult<TPayload> {
13
28
  const { isOnline } = useNetworkStatus();
14
29
  const handlerRef = useRef(options?.handler);
15
30
  handlerRef.current = options?.handler;
16
31
 
32
+ const [status, setStatus] = useState<MutationStatus>('idle');
33
+ const [error, setError] = useState<Error | null>(null);
34
+
17
35
  // Register per-action handler (persists even after unmount)
18
36
  useEffect(() => {
19
37
  if (handlerRef.current) {
@@ -23,7 +41,12 @@ export function useOfflineMutation<TPayload>(
23
41
  }
24
42
  }, [actionName]);
25
43
 
26
- const mutateOffline = async (payload: TPayload) => {
44
+ const reset = useCallback(() => {
45
+ setStatus('idle');
46
+ setError(null);
47
+ }, []);
48
+
49
+ const mutateOffline = useCallback(async (payload: TPayload) => {
27
50
  // Resolve which handler to use: per-action handler > global onSyncAction
28
51
  const handler = handlerRef.current || OfflineManager.getHandler(actionName);
29
52
  const globalHandler = OfflineManager.onSyncAction;
@@ -32,6 +55,8 @@ export function useOfflineMutation<TPayload>(
32
55
  if (isOnline && hasHandler) {
33
56
  // ── ONLINE: Execute directly, skip the queue ──
34
57
  if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (direct)`);
58
+ setStatus('loading');
59
+ setError(null);
35
60
  try {
36
61
  if (handler) {
37
62
  await handler(payload);
@@ -44,20 +69,37 @@ export function useOfflineMutation<TPayload>(
44
69
  retryCount: 0,
45
70
  });
46
71
  }
72
+ setStatus('success');
47
73
  options?.onOptimisticSuccess?.(payload);
48
- } catch (error: any) {
49
- console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, error);
74
+ options?.onSuccess?.(payload);
75
+ } catch (err: any) {
76
+ console.warn(`[OfflineQueue] mutate: ${actionName} failed, falling back to queue`, err);
77
+ // API failed even though online → fallback to queue
50
78
  await OfflineManager.push(actionName, payload);
79
+ setStatus('queued');
80
+ setError(err);
51
81
  options?.onOptimisticSuccess?.(payload);
52
- options?.onError?.(error, payload);
82
+ options?.onError?.(err, payload);
53
83
  }
54
84
  } else {
55
85
  // ── OFFLINE: Add to queue + optimistic update ──
56
86
  if (__DEV__) console.log(`[OfflineQueue] mutate: ${actionName} (queued)`);
57
87
  await OfflineManager.push(actionName, payload);
88
+ setStatus('queued');
89
+ setError(null);
58
90
  options?.onOptimisticSuccess?.(payload);
59
91
  }
60
- };
92
+ }, [actionName, isOnline, options]);
61
93
 
62
- return { mutateOffline };
94
+ return {
95
+ mutateOffline,
96
+ status,
97
+ isIdle: status === 'idle',
98
+ isLoading: status === 'loading',
99
+ isSuccess: status === 'success',
100
+ isError: status === 'error',
101
+ isQueued: status === 'queued',
102
+ error,
103
+ reset,
104
+ };
63
105
  }