react-native-offline
Handful of utilities you should keep in your toolbelt to handle offline/online connectivity in React Native. It supports iOS, Android and Windows platforms. You can leverage all the functionalities provided or just the ones that suits your needs, the modules are conveniently decoupled.
Example app
A comprehensive example app is available within Expo to play with the library and better understand its different modules. Go and check it out!
Contents
Motivation
When you are building your React Native app, you have to expect that some users may use your application in offline mode, for instance when travelling on a Plane (airplane mode) or the underground (no signal). How does your app behave in that situation? Does it show an infinite loader? Can the user still use it seamlessly?
Having an offline first class citizen app is very important for a successful user experience. React Native ships with the NetInfo
module in order to detect internet connectivity. The API is pretty basic and it may be sufficient for small apps but its usage gets cumbersome as your app grows. Besides that, it only detects network connectivity and does not guarantee internet access so it can provide false positives.
This library aims to gather a variety of modules that follow React and Redux best practises, in order to make your life easier when it comes to deal with internet connectivity in your React Native application.
Features
- Offline/online conditional rendering through Provider/Consumer components that leverage the new React Context API
- Reducer to keep your connectivity state in the Redux store
- Redux middleware to intercept internet request actions in offline mode and apply DRY principle
- Compatibility with async middleware libraries like redux-thunk, redux-saga and redux-observable
- A saga to place the network event subscriptions outside of your components
- A step further than
NetInfo
detecting internet access besides network connectivity - Offline queue support to automatically re-dispatch actions when connection is back online or dismiss actions based on other actions dispatched (i.e navigation related)
- Ability to check connectivity regularly
- 100% unit test coverage
Contributions
PRs are more than welcome. If you're planning to contribute please make sure to read the contributing guide: CONTRIBUTING.md
If you use this library on your commercial/personal projects, you can help us by funding the work on specific issues that you choose by using IssueHunt.io!
This gives you the power to prioritize our work and support the project contributors. Moreover it'll guarantee the project will be updated and maintained in the long run.
Sponsors will be listed in the contributors section at the bottom. If you want to be removed please contact me at: rauliyohmc@gmail.com
Installation
RN >= 0.59.x
Make sure to have react-redux
version 6.x.x or 7.x.x installed.
$ yarn add react-native-offline
# Or if you use npm
$ npm i --save react-native-offline
This library uses @react-native-community/netinfo@4.x.x
version underneath the hood. You then need to link the native parts of the library for the platforms you are using.
If you are on React Native v0.60, you don't need to do anything else, since it supports autolinking. For iOS, just go to the ios
folder and run pod install
. However, autolinking might not pick up and install the @react-native-community/netinfo
dependency. If that happens, first install @react-native-community/netinfo
directly, then run pod install
, then install react-native-offline
and finish with pod install
.
Otherwise, the easiest way to link the library is using the CLI tool by running this command from the root of your project:
react-native link @react-native-community/netinfo
If you can't or don't want to use the CLI tool, you can also manually link the library using the instructions below (click on the arrow to show them):
Manually link the library on iOS
Either follow the instructions in the React Native documentation to manually link the framework or link using Cocoapods by adding this to your Podfile
:
pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo'
Manually link the library on Android
Make the following changes:
android/settings.gradle
include ':react-native-community-netinfo'
project(':react-native-community-netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
android/app/build.gradle
dependencies {
...
implementation project(':react-native-community-netinfo')
}
android/app/src/main/.../MainApplication.java
On top, where imports are:
import com.reactnativecommunity.netinfo.NetInfoPackage;
Add the NetInfoPackage
class to your list of exported packages.
@Override
protected List<ReactPackage> getPackages() {
return Arrays.asList(
new MainReactPackage(),
new NetInfoPackage()
);
}
Last but not list, you need to use jetifier to convert the native dependency to AndroidX.
RN >= 0.55.x && RN <= 0.58.x
Make sure to have react-redux
version 6.x.x or 7.x.x installed.
$ yarn add react-native-offline@4.3.2
# Or if you use npm
$ npm i --save react-native-offline@4.3.2
Android
To request network info in Android an extra step is required, so you should add the following line to your app's AndroidManifest.xml
as well:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
API
Component Utilities
In order to render stuff conditionally with ease. They internally listen to connection changes and also provide an extra layer of reliability by ensuring there is internet access when reporting online. For that, an extra request is made to a remote server.
NetworkProvider
Provider component that injects the network state to children components via React Context. Only children prop is required, the rest are optional. It should be used on top of your components hierarchy, ideally in (or close to) the entry point.
type Props = {
children: React.Node,
pingTimeout?: number = 10000,
pingServerUrl?: string = 'https://www.google.com/',
shouldPing?: boolean = true,
pingInterval?: number = 0,
pingOnlyIfOffline?: boolean = false,
pingInBackground?: boolean = false,
httpMethod?: HTTPMethod = 'HEAD',
}
Config
children
: a React Element. This is the only required prop.
pingTimeout
: amount of time (in ms) that the component should wait for the ping response. Defaults to 10000
ms. If you want to use a different value, it's recommended to use a higher one.
pingServerUrl
: remote server to ping to. Defaults to https://www.google.com/
since it's probably one the most stable servers out there, but you can provide your own if needed.
shouldPing
: flag that denotes whether the extra ping check will be performed or not. Defaults to true
.
pingInterval
: the interval (in ms) you want to ping the server at. Defaults to 0
, and that means it is not going to check connectivity regularly. If opted in, it's advised not to choose a very small value, because that may drain your battery. Choose wisely. Something around 30000 ms should be fine.
pingOnlyIfOffline
: when set to true
and pingInterval
> 0, it will ping the remote server regularly only if offline. Defaults to false
.
pingInBackground
: whether or not to check connectivity when app isn't in the foreground. Defaults to false
.
httpMethod
: http method used to ping the server. Supports HEAD or OPTIONS. Defaults to HEAD
.
Usage
import React from 'react';
import { NetworkProvider } from 'react-native-offline';
import App from './App';
const Root = () => (
<NetworkProvider>
<App />
</NetworkProvider>
);
export default Root;
NetworkConsumer
React component that subscribes to connectivity changes. It requires a function as a child. The function receives the current connectivity status and returns a React node. This component should be rendered within a NetworkProvider in order to work properly.
Props
type NetworkState = {
isConnected: boolean,
}
type Props = {
children: ({ isConnected }: NetworkState) => React.Node
}
Usage
import React from 'react';
import { Image, Button, Text } from 'react-native';
import { NetworkConsumer } from 'react-native-offline';
const ImageViewer = () => (
<View>
<Image src="foo.com" />
<NetworkConsumer>
{({ isConnected }) => (
isConnected ? (
<Button title="Download image" onPress={downloadImage} />
) : (
<Text>Downloading images is disabled since you are offline</Text>
)
)}
</NetworkConsumer>
</View>
);
Integration with Redux
There are 3 features that this library provides in order to leverage offline capabilities in your Redux store: a reducer, a middleware and an offline queue system. You can use all of them or just the ones that suits your needs.
Network reducer
A network reducer to be provided to the store.
State
type NetworkState = {
isConnected: boolean,
actionQueue: Array<*>
}
Usage
1.- Give the network reducer to Redux
import { createStore, combineReducers } from 'redux'
import { reducer as network } from 'react-native-offline';
const rootReducer = combineReducers({
network,
});
const store = createStore(rootReducer);
export default store;
2.- Here you have 2 options:
ReduxNetworkProvider
Uses a provider component mechanism. The same props as for NetworkProvider
apply. Make sure your component is a descendant of the react-redux <Provider>
component, so that ReduxNetworkProvider
has access to the store.
import store from './reduxStore';
import React from 'react';
import { Provider } from 'react-redux';
import { ReduxNetworkProvider } from 'react-native-offline';
let App = () => (
<Navigator>
<MainScreen />
<OtherScreen />
</Navigator>
);
const Root = () => (
<Provider store={store}>
<ReduxNetworkProvider>
<App />
</ReduxNetworkProvider>
</Provider>
);
networkSaga
Just fork this saga from your root saga. It accepts the same config options as NetworkProvider
and ReduxNetworkProvider
. Recommended if you are using redux-saga, since it's a very elegant way to deal with global connectivity changes, without having to wrap your components with extra functionality.
import { all } from 'redux-saga/effects';
import saga1 from './saga1';
import saga2 from './saga2';
import { networkSaga } from 'react-native-offline';
export default function* rootSaga(): Generator<*, *, *> {
yield all([
fork(saga1),
fork(saga2),
fork(networkSaga, { pingInterval: 20000 }),
]);
}
3.- Access your network state in your components using mapStateToProps()
, as state.network.isConnected
.
Note: If you wanna listen to the action dispatched internally in your reducers, import the offline action types and reference CONNECTION_CHANGE
:
import { offlineActionTypes } from 'react-native-offline';
...
if(action.type === offlineActionTypes.CONNECTION_CHANGE)
...
createNetworkMiddleware()
Function that returns a Redux middleware which listens to specific actions targeting API calls in online/offline mode.
createNetworkMiddleware(config: MiddlewareConfig): ReduxMiddleware
type MiddlewareConfig = {
regexActionType?: RegExp = /FETCH.*REQUEST/,
actionTypes?: Array<string> = [],
queueReleaseThrottle?: number = 50,
}
PO Config
This is the setup you need to put in place for libraries such as redux-saga
or redux-observable
, which rely on plain actions being dispatched to trigger async flow:
regexActionType
: regular expression to indicate the action types to be intercepted in offline mode.
By default it's configured to intercept actions for fetching data following the Redux convention. That means that it will intercept actions with types such as FETCH_USER_ID_REQUEST
, FETCH_PRODUCTS_REQUEST
etc.
actionTypes
: array with additional action types to intercept that don't fulfil the RegExp criteria. For instance, it's useful for actions that carry along refreshing data, such as REFRESH_LIST
.
queueReleaseThrottle
: waiting time in ms between dispatches when flushing the offline queue. Useful to reduce the server pressure when coming back online. Defaults to 50ms.
Thunks Config
For redux-thunk
library, the async flow is wrapped inside functions that will be lazily evaluated when dispatched, so our store is able to dispatch functions as well. Therefore, the configuration differs:
- Make sure to use a named function instead of an anonymous arrow function for the thunk returned by your action creator.
- Use
interceptInOffline
property in your thunk and set it to true
.
Example:
export const fetchUser = (url) => {
function thunk(dispatch) {
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
dispatch({type: FETCH_USER_SUCCESS, payload: responseJson});
})
.catch((error) => {
console.error(error);
});
};
thunk.interceptInOffline = true;
return thunk;
};
Usage
You should apply the middleware to the store using applyMiddleware
. The network middleware should be the first on the chain, before any other middleware used for handling async actions, such as redux-thunk, redux-saga or redux-observable.
import { createStore, applyMiddleware } from 'redux';
import { createNetworkMiddleware } from 'react-native-offline';
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
const networkMiddleware = createNetworkMiddleware({
queueReleaseThrottle: 200,
});
const store = createStore(
rootReducer,
applyMiddleware(networkMiddleware, sagaMiddleware)
);
When you attempt to fetch data on the internet by means of dispatching a plain action or a thunk in offline mode, the middleware blocks the action and dispatches an action of type @@network-connectivity/FETCH_OFFLINE_MODE
instead, containing useful information about "what you attempted to do". The action dispatched signature for plain objects is as follows:
type FetchOfflineModeActionForPO = {
type: '@@network-connectivity/FETCH_OFFLINE_MODE',
payload: {
prevAction: {
type: string,
payload?: any,
}
}
}
And for thunks it attaches it under prevThunk
property:
type FetchOfflineModeActionForThunks = {
type: '@@network-connectivity/FETCH_OFFLINE_MODE',
payload: {
prevThunk: Function
}
}
That allows you to react conveniently and update your state in the way you desire, based on your previous intent. Just reference FETCH_OFFLINE_MODE
action type in your reducer:
import { offlineActionTypes } from 'react-native-offline';
...
if(action.type === offlineActionTypes.FETCH_OFFLINE_MODE)
...
SnackBars, Dialog, Popups, or simple informative text are good means of conveying to the user that the operation failed due to lack of internet connection.
Offline Queue
A queue system to store actions that failed due to lack of connectivity. It works for both plain object actions and thunks. It allows you to:
- Re-dispatch the action/thunk as soon as the internet connection is back online again
- Dismiss the action from the queue based on a different action dispatched (i.e. navigating to a different screen, the fetch action is no longer relevant)
Managing duplicate actions
If a similar action already exists on the queue, we remove it and push it again to the end, so it has an overriding effect.
The default criteria to detect duplicates is by using lodash.isEqual
for plain actions and thunk.toString()
for thunks/functions. However, you can customise the comparison function to acommodate it to your needs. For that, you need to use the factory version for your network reducer.
import { createStore, combineReducers } from 'redux'
import { createReducer as createNetworkReducer } from 'react-native-offline';
import { comparisonFn } from './utils';
const rootReducer = combineReducers({
createNetworkReducer(comparisonFn),
});
const store = createStore(rootReducer);
export default store;
The comparison function receives the action dispatched when offline and the current actionQueue
. The result of the function will be either undefined
, meaning no match found, or the action that matches the passed in action. So basically, you need to return the upcoming action if you wish to replace an existing one. An example of how to use it can be found here.
function comparisonFn(
action: ReduxAction | ReduxThunk,
actionQueue: Array<ReduxAction | ReduxThunk>,
): ?(ReduxAction | ReduxThunk)
Plain Objects
In order to configure your PO actions to interact with the offline queue you need to use the meta
property in your actions, following flux standard actions convention. They need to adhere to the below API:
type ActionToBeQueued = {
type: string,
payload?: any,
meta: {
retry?: boolean,
dismiss?: Array<string>
}
}
Examples
- Action that will be added to the queue on offline mode and that will be re-dispatched as soon as the connection is back online again
const action = {
type: 'FETCH_USER_ID',
payload: {
id: 2
},
meta: {
retry: true
}
};
- Action that will be added to the queue on offline mode and that will be re-dispatched as soon as the connection is back online again, as long as a
NAVIGATE_BACK
action type hasn't been dispatched in between, in which case the action would be removed from the queue.
const action = {
type: 'FETCH_USER_ID',
payload: {
id: 2
},
meta: {
retry: true,
dismiss: ['NAVIGATE_BACK']
}
};
Thunks
- For thunks, append
interceptInOffline
and meta
properties to the function returned by the action creator, where meta
has the same shape as for Flux actions:
function fetchData(dispatch, getState) {
dispatch({ type: FETCH_USER_ID_REQUEST, payload: { id: '3' } });
...
}
fetchData.interceptInOffline = true;
fetchData.meta = {
retry?: boolean,
dismiss?: Array<string>
}
Other utilities
checkInternetConnection()
Utility function that allows you to query for internet connectivity on demand. If you have integrated this library with redux, you can then dispatch a CONNECTION_CHANGE
action type to inform the network
reducer accordingly and keep it up to date. Check the example below.
Note: It's recommended to always set shouldPing
to true
(the default behaviour), in order to prevent inconsistent behaviour on iOS for RN < 0.57.
checkInternetConnection(
url?: string = 'https://www.google.com/',
pingTimeout?: number = 10000,
shouldPing?: boolean = true
): Promise<boolean>
Example
import { checkInternetConnection, offlineActionTypes } from 'react-native-offline';
async function internetChecker(dispatch) {
const isConnected = await checkInternetConnection();
dispatch({
type: offlineActionTypes.CONNECTION_CHANGE,
payload: isConnected,
});
}
Miscellanea
FAQ
How to test offline behavior while actually being online
You can use pingServerUrl
and set it to a non existing url or point to some server that is down. Don't forget to also set shouldPing
to true
(which is the default behaviour).
Don't rely too much on iOS simulators and switching on/off the internet connection on your computer, they are quite buggy and report inconsistent connectivity information. On the other hand, testing on real devices should be fine.
How to orchestrate Redux to dispatch CONNECTION_CHANGE
as the first action when the app starts up
The solution assumes you are using Redux Persist v5.x and involves using some local state in your top most component and tweaking the configureStore
function a bit, so that it can notify your root React component to render the whole application when the required initialisation has taken place. In this case, by initialisation, we are talking about rehydrating the store from disk and detecting initial internet connection.
As you can see in the snippets below, we create the store
instance as usual and return it in our configureStore
function. The only difference is that the function is still alive and will invoke the callback as soon as 2 actions are dispatched into the store (in order):
REHYDRATE
from redux-persist
CONNECTION_CHANGE
from react-native-offline
import { createStore, applyMiddleware } from 'redux';
import { persistStore } from 'redux-persist';
import { createNetworkMiddleware, offlineActionTypes, checkInternetConnection } from 'react-native-offline';
import rootReducer from '../reducers';
const networkMiddleware = createNetworkMiddleware();
export default function configureStore(callback) {
const store = createStore(rootReducer, applyMiddleware(networkMiddleware));
persistStore(store, null, () => {
checkInternetConnection().then(isConnected => {
store.dispatch({
type: offlineActionTypes.CONNECTION_CHANGE,
payload: isConnected,
});
callback();
});
});
return store;
}
Then, our root React component will have some local state, that initially will impose the component to return null
, waiting until the async operations complete. Then, we trigger a setState
to render the application.
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import configureStore from './store';
import Root from './Root';
class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
store: configureStore(() => this.setState({ isLoading: false })),
};
}
render() {
if (this.state.isLoading) return null;
return (
<Provider store={this.state.store}>
<Root />
</Provider>
);
}
}
export default App;
This way, we make sure the right actions are dispatched before anything else can be.
How to intercept and queue actions when the server responds with client (4xx) or server (5xx) errors
You can do that by dispatching yourself an action of type @@network-connectivity/FETCH_OFFLINE_MODE
. The action types the library uses are exposed under offlineActionTypes
property.
Unfortunately, the action creators are not exposed yet, so I'll release soon a new version with that fixed. In the meantime, you can check that specific action creator in here, so that you can emulate its payload. That should queue up your action properly.
import { offlineActionTypes } from 'react-native-offline';
...
fetch('someurl/data').catch(error => {
dispatch({
type: actionTypes.FETCH_OFFLINE_MODE,
payload: {
prevAction: {
type: action.type,
payload: action.payload,
},
},
meta: { retry: true }
})
);
How to persist and rehydrate thunks in the offline queue with Redux Persist
Due to the way Redux Persist serializes the store, persisting and rehydrating thunks will return an invalid action. Fortunately, there is a workaround.
In your action creator, make sure to format it as specified from the thunks config with a couple of additions.
export const fetchUser = (url) => {
function thunk(dispatch) {
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
dispatch({type: FETCH_USER_SUCCESS, payload: responseJson});
})
.catch((error) => {
console.error(error);
});
};
thunk.interceptInOffline = true;
thunk.meta = {
retry: true,
name: 'fetchUser',
args: [url],
};
return thunk;
};
Add the following into your redux store. Refer to the transforms section for more information on how Redux Persist transforms data.
import { fetchUser } from './actions.js';
import { fetchOtherUsers } from './otherActions.js';
const actions = {
fetchUser,
fetchOtherUsers,
};
const networkTransform = createTransform(
(inboundState, key) => {
const actionQueue = [];
inboundState.actionQueue.forEach(action => {
if (typeof action === 'function') {
actionQueue.push({
function: action.meta.name,
args: action.meta.args,
});
} else if (typeof action === 'object') {
actionQueue.push(action);
}
});
return {
...inboundState,
actionQueue,
};
},
(outboundState, key) => {
const actionQueue = [];
outboundState.actionQueue.forEach(action => {
if (action.function) {
const actionFunction = actions[action.function];
actionQueue.push(actionFunction(...action.args));
} else {
actionQueue.push(action);
}
});
return { ...outboundState, actionQueue };
},
{ whitelist: ['network'] },
);
const persistConfig = {
key: 'root',
storage,
transforms: [networkTransform],
};
Using redux-saga 1.0.0-beta.x
If you are using a 1.0.0-beta.x
version for redux-saga in your application, you may have some conflicts when yarn install dependencies, since this library relies on the latest stable version 0.16.2
and that could take precedence on your node_modules
. In order to fix it, you can use yarn resolutions by adding the next lines of code to your package.json
, where x
is the beta version number:
"resolutions": {
"react-native-offline/redux-saga": "^1.0.0-beta.x"
},
Inspiration
Thanks to Spencer Carli for his awesome article about Handling Offline actions in React Native, which served me as inspiration for the offline queue implementation.
License
MIT
Contributors
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!