Last active
July 21, 2021 13:41
-
-
Save Grohden/750cd826a1d09044889c2f94dbc81f14 to your computer and use it in GitHub Desktop.
Simple offline queue implementation for react native
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createAsyncThunk } from '@reduxjs/toolkit'; | |
import type { AppDispatch, RootState } from './my-store-file'; | |
import { useAppDispatch } from './my-store-file'; | |
import { useCallback } from 'react'; | |
import { useOfflineExecutor } from './offline-executor'; | |
import { createItem } from './create-item-slice.ts'; | |
import { ItemService } from './services'; | |
export type CreateItemThunkArg = { | |
name: string | |
}; | |
export const useCreateItem = () => { | |
const dispatch = useAppDispatch() | |
const { addToOfflineQueue } = useOfflineExecutor(); | |
return useCallback( | |
(item: CreateItemThunkArg) => { | |
dispatch(createItem({ offline: true, item })) | |
addToOfflineQueue({ | |
name: 'createItem', | |
payload: item, | |
}); | |
}, | |
[addToOfflineQueue], | |
); | |
}; | |
export const createItemThunk = createAsyncThunk< | |
void, | |
CreateItemThunkArg, | |
{ dispatch: AppDispatch; state: RootState } | |
>('attendance/createChecklistAnswers', async (item, { dispatch }) => { | |
const { data } = await ItemService.create(item); | |
dispatch(createItem({ offline: false, item: data })) | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useNetInfo } from '@react-native-community/netinfo'; | |
import type { ScheduledAction } from '_ducks/offline/offline-slice'; | |
import { | |
addInQueue, | |
dispatchMappings, | |
removeIdsFromQueue, | |
} from './offline-slice'; | |
import { useAppDispatch, useAppSelector } from '_store/hooks'; | |
import type { ReactNode } from 'react'; | |
import React, { | |
createContext, | |
useCallback, | |
useContext, | |
useEffect, | |
useMemo, | |
useRef, | |
} from 'react'; | |
import createUUID from '_utils/create-uuid'; | |
type ContextValue = { | |
addToOfflineQueue: (item: Omit<ScheduledAction, 'id'>) => void; | |
releaseQueue: () => Promise<void>; | |
}; | |
const noop = () => {}; | |
const OfflineExecutorContext = createContext<ContextValue>({ | |
releaseQueue: () => Promise.resolve(), | |
addToOfflineQueue: noop, | |
}); | |
export const OfflineExecutor = ({ children }: { children: ReactNode }) => { | |
const { isConnected } = useNetInfo(); | |
const { queue } = useAppSelector((store) => store.offline); | |
const dispatch = useAppDispatch(); | |
const executing = useRef(false); | |
const queueRef = useRef(queue); | |
queueRef.current = queue; | |
const releaseQueue = useCallback(async () => { | |
const successful: string[] = []; | |
if (executing.current || !queueRef.current.length) { | |
return; | |
} | |
__DEV__ && console.log('[offline] releasing queue'); | |
executing.current = true; | |
for (const item of queueRef.current) { | |
try { | |
const mapping = dispatchMappings[item.name]; | |
// @ts-ignore: too tired to type anything here | |
await dispatch(mapping(item.payload as any)).unwrap(); | |
successful.push(item.id); | |
} catch (e) { | |
console.log(e); | |
// todo: this may cause an infinite execution loop... | |
// need to deal with only network issues | |
} | |
} | |
if (successful.length) { | |
dispatch(removeIdsFromQueue(successful)); | |
} | |
executing.current = false; | |
}, [dispatch]); | |
// periodically try to execute | |
useEffect(() => { | |
if (!isConnected) { | |
return; | |
} | |
const interval = setInterval(() => { | |
void releaseQueue(); | |
}, 3000); | |
return () => { | |
clearInterval(interval); | |
}; | |
}, [dispatch, isConnected, releaseQueue]); | |
const addToOfflineQueue = useCallback( | |
(action: Omit<ScheduledAction, 'id'>) => { | |
dispatch(addInQueue({ id: createUUID(), ...action })); | |
}, | |
[dispatch], | |
); | |
const value = useMemo( | |
() => ({ | |
addToOfflineQueue, | |
releaseQueue, | |
}), | |
[addToOfflineQueue, releaseQueue], | |
); | |
return ( | |
<OfflineExecutorContext.Provider value={value}> | |
{children} | |
</OfflineExecutorContext.Provider> | |
); | |
}; | |
export const useOfflineExecutor = () => useContext(OfflineExecutorContext); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { AsyncThunk, PayloadAction } from '@reduxjs/toolkit'; | |
import { createSlice } from '@reduxjs/toolkit'; | |
import type { AppDispatch, RootState } from './my-store-file'; | |
import type { CreateItemThunkArg } from './create-item.executor'; | |
import { createItemThunk } from './create-item.executor'; | |
type DispatchMappings = { | |
createItem: CreateItemThunkArg; | |
}; | |
export type ScheduledAction< | |
T extends keyof DispatchMappings = keyof DispatchMappings | |
> = { | |
id: string; | |
name: T; | |
payload: DispatchMappings[T]; | |
}; | |
export const dispatchMappings: { | |
[key in keyof DispatchMappings]: AsyncThunk< | |
void, | |
DispatchMappings[key], | |
{ dispatch: AppDispatch; state: RootState } | |
>; | |
} = { | |
createItem: createItemThunk, | |
}; | |
type OfflineState = { | |
queue: ScheduledAction[]; | |
}; | |
const initialState: OfflineState = { | |
queue: [], | |
}; | |
const offlineSlice = createSlice({ | |
name: 'offline', | |
initialState, | |
reducers: { | |
setQueue: (state, action: PayloadAction<ScheduledAction[]>) => { | |
state.queue = action.payload; | |
}, | |
addInQueue: (state, action: PayloadAction<ScheduledAction>) => { | |
state.queue = [...state.queue, action.payload]; | |
}, | |
removeIdsFromQueue: (state, action: PayloadAction<string[]>) => { | |
state.queue = state.queue.filter( | |
(item) => !action.payload.includes(item.id), | |
); | |
}, | |
}, | |
}); | |
export const { | |
setQueue, | |
addInQueue, | |
removeIdsFromQueue, | |
} = offlineSlice.actions; | |
export const offlineReducer = offlineSlice.reducer; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment