react-native-offline
Version:
Handy toolbelt to deal with offline mode in React Native applications. Cross-platform, provides a smooth redux integration.
273 lines (257 loc) • 7.09 kB
text/typescript
import { put, select, call, take, cancelled, fork } from 'redux-saga/effects';
import { eventChannel, Subscribe } from 'redux-saga';
import { AppState, Platform } from 'react-native';
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
import { networkSelector } from './createReducer';
import checkInternetAccess from '../utils/checkInternetAccess';
import { connectionChange } from './actionCreators';
import { ConnectivityArgs, NetworkState } from '../types';
import {
DEFAULT_TIMEOUT,
DEFAULT_PING_SERVER_URL,
DEFAULT_HTTP_METHOD,
DEFAULT_ARGS,
} from '../utils/constants';
type NetInfoChangeArgs = Omit<
ConnectivityArgs,
'pingInterval' | 'pingOnlyIfOffline' | 'pingInBackground'
>;
type CheckInternetArgs = Omit<NetInfoChangeArgs, 'shouldPing'> & {
pingInBackground: boolean;
};
export function netInfoEventChannelFn(emit: (param: NetInfoState) => unknown) {
return NetInfo.addEventListener(emit);
}
export function intervalChannelFn(interval: number) {
return (emit: (param: boolean) => unknown) => {
const iv = setInterval(() => emit(true), interval);
return () => {
clearInterval(iv);
};
};
}
/**
* Returns a factory function that creates a channel from network connection change events
* @returns {Channel<T>}
*/
export function createNetInfoConnectionChangeChannel<T = any>(
channelFn: Subscribe<T>,
) {
return eventChannel(channelFn);
}
/**
* Returns a factory function that creates a channel from an interval
* @param interval
* @returns {Channel<T>}
*/
export function createIntervalChannel(interval: number, channelFn: Function) {
const handler = channelFn(interval);
return eventChannel(handler);
}
/**
* Creates a NetInfo change event channel that:
* - Listens to NetInfo connection change events
* - If shouldPing === true, it first verifies we have internet access
* - Otherwise it calls handleConnectivityChange immediately to process the new information into the redux store
* @param pingTimeout
* @param pingServerUrl
* @param shouldPing
* @param httpMethod
* @param customHeaders
*/
export function* netInfoChangeSaga({
pingTimeout,
pingServerUrl,
shouldPing,
httpMethod,
customHeaders,
}: NetInfoChangeArgs) {
if (Platform.OS === 'android') {
const networkState: NetInfoState = yield call([NetInfo, NetInfo.fetch]);
yield fork(connectionHandler, {
shouldPing,
isConnected: networkState.isConnected,
pingTimeout,
pingServerUrl,
httpMethod,
customHeaders,
});
}
const chan = yield call(
createNetInfoConnectionChangeChannel,
netInfoEventChannelFn,
);
try {
while (true) {
const isConnected = yield take(chan);
yield fork(connectionHandler, {
shouldPing,
isConnected,
pingTimeout,
pingServerUrl,
httpMethod,
customHeaders,
});
}
} finally {
if (yield cancelled()) {
chan.close();
}
}
}
/**
* Either checks internet by pinging a server or calls the store handler function
* @param shouldPing
* @param isConnected
* @param pingTimeout
* @param pingServerUrl
* @param httpMethod
* @param customHeaders
* @returns {IterableIterator<ForkEffect | *>}
*/
export function* connectionHandler({
shouldPing,
isConnected,
pingTimeout,
pingServerUrl,
httpMethod,
customHeaders,
}: NetInfoChangeArgs & { isConnected: boolean | null }) {
if (shouldPing && isConnected) {
yield fork(checkInternetAccessSaga, {
pingTimeout,
pingServerUrl,
httpMethod,
pingInBackground: false,
customHeaders,
});
} else {
yield fork(handleConnectivityChange, isConnected);
}
}
/**
* Creates an interval channel that periodically verifies internet access
* @param pingTimeout
* @param pingServerUrl
* @param interval
* @param pingOnlyIfOffline
* @param pingInBackground
* @param httpMethod
* @param customHeaders
* @returns {IterableIterator<*>}
*/
export function* connectionIntervalSaga({
pingTimeout,
pingServerUrl,
pingInterval,
pingOnlyIfOffline,
pingInBackground,
httpMethod,
customHeaders,
}: Omit<ConnectivityArgs, 'shouldPing'>) {
const chan = yield call(
createIntervalChannel,
pingInterval,
intervalChannelFn,
);
try {
while (true) {
yield take(chan);
const state: NetworkState = yield select(networkSelector);
if (!(state.isConnected && pingOnlyIfOffline === true)) {
yield fork(checkInternetAccessSaga, {
pingTimeout,
pingServerUrl,
httpMethod,
pingInBackground,
customHeaders,
});
}
}
} finally {
if (yield cancelled()) {
chan.close();
}
}
}
/**
* Saga that verifies internet connection, besides connectivity, by pinging a server of your choice
* @param pingServerUrl
* @param pingTimeout
* @param httpMethod
* @param pingInBackground
* @param customHeaders
*/
export function* checkInternetAccessSaga({
pingServerUrl,
pingTimeout,
httpMethod,
pingInBackground,
customHeaders,
}: CheckInternetArgs) {
if (pingInBackground === false && AppState.currentState !== 'active') {
return; // <-- Return early as we don't care about connectivity if app is not in foreground.
}
const hasInternetAccess = yield call(checkInternetAccess, {
url: pingServerUrl,
timeout: pingTimeout,
method: httpMethod,
customHeaders,
});
yield call(handleConnectivityChange, hasInternetAccess);
}
/**
* Takes action under the new network connection value:
* - Dispatches a '@@network-connectivity/CONNECTION_CHANGE' action type
* - Flushes the queue of pending actions if we are connected back to the internet
* @param hasInternetAccess
*/
export function* handleConnectivityChange(hasInternetAccess: boolean | null) {
const state: NetworkState = yield select(networkSelector);
if (state.isConnected !== hasInternetAccess) {
yield put(connectionChange(hasInternetAccess));
}
}
/**
* Saga that controls internet connectivity in your whole application.
* You just need to fork it from your root saga.
* It receives the same parameters as withNetworkConnectivity HOC
* @param pingTimeout
* @param pingServerUrl
* @param shouldPing
* @param pingInterval
* @param pingOnlyIfOffline
* @param pingInBackground
* @param httpMethod
* @param customHeaders
*/
export default function* networkSaga(args?: ConnectivityArgs) {
const {
pingTimeout = DEFAULT_TIMEOUT,
pingServerUrl = DEFAULT_PING_SERVER_URL,
pingInterval = 0,
shouldPing = true,
pingOnlyIfOffline = false,
pingInBackground = false,
httpMethod = DEFAULT_HTTP_METHOD,
customHeaders,
} = args || DEFAULT_ARGS;
yield fork(netInfoChangeSaga, {
pingTimeout,
pingServerUrl,
shouldPing,
httpMethod,
customHeaders,
});
if (pingInterval > 0) {
yield fork(connectionIntervalSaga, {
pingTimeout,
pingServerUrl,
pingInterval,
pingOnlyIfOffline,
pingInBackground,
httpMethod,
customHeaders,
});
}
}