Skip to content

Instantly share code, notes, and snippets.

@Grohden
Last active July 21, 2021 13:41
Show Gist options
  • Save Grohden/750cd826a1d09044889c2f94dbc81f14 to your computer and use it in GitHub Desktop.
Save Grohden/750cd826a1d09044889c2f94dbc81f14 to your computer and use it in GitHub Desktop.
Simple offline queue implementation for react native
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 }))
});
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);
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