Your React Native Offline Tool Belt

Authors
Raul Gomez Acuna
Software Engineer
@
Callstack

Having worked with React for a while, I am currently building a medium-sized React Native application in my free time, simply to try out the platform and because I truly love building mobile apps with minimum effort. Mobile apps require you to cover things that may be easily overlooked if you are even a bit careless.

One of the expectations is that some users may use your application in offline mode. How does your app behave when a user is traveling on a plane (airplane mode) or the underground (no signal)? Does it show an infinite loader? Can the user still use the app seamlessly?

Having an offline first-class citizen app is very important for a great user experience. You may have heard about (or used) NetInfo module, which comes shipped in with React Native and provides a handful of methods to get the network connection status on the device. The API is pretty basic and it may be sufficient for small apps but its usage gets cumbersome as your app grows.

When that happens, you’ll find yourself repeating code in the two following scenarios:

  • Inside Component’s render: rendering one thing or another depending on whether I have internet connection or not
  • Inside your business logic: grabbing connection state and conditionally fetching data

Besides that, NetInfo presents another downside. Its API allows you to detect network connectivity, but it does not guarantee you can exchange data with a remote server even though it reports you are connected. This scenario could occur inside an airport where there are a lot of available networks that you could connect to, though forcing you to go through an extra step (like filling a form) in order to gain internet access.

As I was trying to find a more compact solution to this problem, react-native-offline was born. I’ll elaborate on the different parts of the library in the context of the app I was building.

Components utilities

There are occasions when you would want to hide or change content on the screen depending on whether there is internet connection or not. Let’s analyze a couple of examples based on an events app.

Let’s say we would like to know all the events for a particular day. If there is no internet connection, we will instead show to the user a text message stating we are in offline mode. In this case, the library provides you with a Higher Order Component utility that passes the connection state down as a prop. You’d use it the following way:

import React from 'react';
import { withNetworkConnectivity } from 'react-native-offline';
import { View, Text } from 'react-native';
import EventList from './EventList';
const EventsForDay = ({ isConnected, day }) => (
<View style={{ flex: 1 }}>
{isConnected ? <EventList day={day} /> : <Text>We are offline :(</Text>}
</View>
);
export default withNetworkConnectivity()(EventsForDay);
view raw EventsForDay.js hosted with ❤ by GitHub

Let’s think now of a slightly different scenario. Imagine an events detail page where you can rsvp and also check the event location through a map. In case of a lack of connection and assuming we are persisting our data, the user should still be able to check event details, but we’d like to tweak the screen content a bit by preventing the user from performing network dependent actions.

In this case, our goal is to disable the rsvp buttons, hide the map (assuming we have a service to get the geolocation coordinates based on the address) and, in addition, show a SnackBar at the bottom to inform the user that some of the functionalities on that screen may be disabled.

If we wanna keep the rendering of the screen in one file, a Higher Order Component won’t work. In such a case the library provides a function as a child component (a.k.a. rendering callback) as a convenience.

import React, { Component } from 'react';
import {
View,
ScrollView,
StyleSheet,
} from 'react-native';
import { ConnectivityRenderer } from 'react-native-offline';
// Rest of component imports ...
class EventDetailsScreen extends Component {
render() {
const { event } = this.props;
return (
<ScrollView style={styles.container}>
<EventCover uri={event.uri} />
<View style={styles.content}>
<EventBasicInformation
data={event.basicInfo}
/>
<ConnectivityRenderer>
{isConnected => isConnected && <EventLocation
address={event.fullAddress}
eventId={event.eventId}
/>}
</ConnectivityRenderer>
<EventDescription description={event.description} />
<ConnectivityRenderer>
{isConnected => <RsvpButtons disabled={!isConnected} />}
</ConnectivityRenderer>
</View>
<ConnectivityRenderer>
{isConnected => (
<SnackBar
message="You are currently offline, some features may be disabled"
predicate={!isConnected}
/>
)}
</ConnectivityRenderer>
</ScrollView>
);
}
}
export default EventDetailsScreen;

You should take into account that since <rte-code>ConnectivityRenderer<rte-code> gives you this power at render time, you typically can’t optimize them using <rte-code>shouldComponentUpdate<rte-code> without hindering your composition ability, so it’s recommended to use it on leaf components in your tree.

Redux integration

For medium and large apps redux is by far the current de facto solution for managing your application state. The library provides a nice integration, by offering three features that 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.

Reducer

It allows you to centralize the connection status in your redux store. Basically it supplies a network reducer that listens to internal actions dispatched when the connection status changes. The configuration is pretty straightforward.

Middleware

Before diving in, let me provide you with a bit of context. In my application I chose redux-saga as the library for managing side effects and network calls and I got to the point where I had around 20 sagas with the following pattern:

import NetInfo from 'react-native';
import { call, put, select, fork, takeEvery } from 'redux-saga';
import * as actions from './actions';
import api from './api';
function* fetchData({ payload }) {
const isConnected = yield call([NetInfo, NetInfo.isConnected.fetch]);
if (isConnected) {
const accessToken = yield select(accessTokenSelector);
try {
const { data } = yield call(api.fetchData, accessToken, payload.params);
yield put(actions.fetchDataSuccess(data));
} catch (error) {
yield put(actions.fetchDataError({
errorType: 'server'
}));
}
} else {
yield put(actions.fetchDataError({
errorType: 'offline',
}));
}
}
export default function* watchFetchData() {
yield fork(takeEvery, 'FETCH_DATA_REQUEST', fetchData);
}
view raw saga.js hosted with ❤ by GitHub

Do you see the repetitive parts? It boils down to grabbing the connection state, the if/else structure for fetching data and dispatching a specific action to handle the offline situation. I live with DRY in mind, so that was annoying me. How could we get rid of that repetition? You guessed it, redux middleware to the rescue.

Redux middleware is the game changer that allows you to place code that sits in between your dispatched actions and the moment they reach the reducer. It’s the perfect place to anticipate the loss of connection and prevent the action from going through the reducer. That way, we have that logic defined in one place only and can clean up a bit our side effects code.

The API exposes a function, createNetworkMiddleware, that returns a redux middleware which you can later apply using applyMiddleware utility from redux:

createNetworkMiddleware(config: Config): ReduxMiddleware
type Config = {
regexActionType?: RegExp = /FETCH.*REQUEST/,
actionTypes?: Array<string> = []
};
view raw ReduxMiddleware hosted with ❤ by GitHub

All parameters are optional and, by default, it will look at your fetch actions types following the redux convention, but you can either provide your own regular expressions and/or any additional action types involving a network call that you are interested to observe. To do that you can use the actionTypesparameter and set it to, for example, AUTHORISE_USER or RELOAD_LIST.

For thunks, the configuration differs. First, you need to make sure of using a named function declaration instead of an anonymous arrow function. Lastly, set interceptInOffline property to true in your thunk as you can see in the below example:

export const fetchUser = (url) => {
return 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;
};

If we attempt to fetch internet resources and happen to be offline, the middleware will stop and prevent the action from going through the rest of the middleware chain, dispatching instead an action of type @@network-connectivity/FETCH_OFFLINE_MODE whose payload contains useful information about “what you attempted to do”.

With this middleware applied to redux, the previous saga would result in:

function* fetchData({ payload }) {
const accessToken = yield select(accessTokenSelector);
try {
const { data } = yield call(api.fetchData, accessToken, payload.params);
yield put(actions.fetchDataSuccess(events));
} catch (error) {
yield put(actions.fetchDataError(error));
}
}
view raw simplerSaga.js hosted with ❤ by GitHub

Isn’t that neat? :) Now you just have to import that action type from the library and listen to it inside your reducers.

Worth saying that the same reduction of code would apply to libraries such as redux-observable or redux-thunk.

Offline queue

Last but not least, the middleware interoperates with an offline queue in order to keep track of actions that failed to be dispatched due to the lack of connection. There are two possible configurations:

  • Retry an action
  • Dismiss an action from the queue based on a different action dispatched

To enable queuing in your redux actions you need to use the <rte-code>meta<rte-code> property, which provides <rte-code>retry<rte-code> and <rte-code>dismiss<rte-code> options.

type ActionToBeQueued = {
type: string,
payload?: any,
meta: {
retry?: boolean,
dismiss?: Array<string>
}
}
view raw retry hosted with ❤ by GitHub

If you set <rte-code>retry: true<rte-code>, the action will be added to the queue and will be automatically re-dispatched when the connection is back again.

However, there are cases where the action queued is no longer relevant. One scenario is navigation actions that involve replacing the entire screen content. For that, the <rte-code>dismiss<rte-code> option is quite handy and you can use it to provide an array of action types that, in case of being dispatched, will dismiss and remove the queued action.

Detecting internet access

We stated before that <rte-code>NetInfo<rte-code> carried the limitation of being only able to detect network connectivity, lacking a way to determine whether we have internet access or not.

To solve that problem, the library takes a step further and covers the ground by pinging a remote server using a http HEAD request, in order to make sure the new connection state is not a false positive. By default, it’s configured to ping google.com with a timeout of 3 seconds, but you can customise both remote server and timeout as you wish. Those parameters can be passed to component utilities and your redux store.

Wrapping up

It’s been a pleasant journey to shape up my first open source library and I am happy to see that other people use it in their projects too. Following the best practices, the library has also almost 100% test coverage in the business logic and is fully typed with Flow. The README.md contains all the instructions to set up the library, full API documentation and some useful examples.

Go ahead, check react-native-offline on GitHub, give it a try, and let me know your thoughts by leaving a comment or opening up issues. PRs are also welcome!

If you liked this article, please recommend it to others. Also, discover services offered by our React Native development company.

Insights

Learn more about Performance

cover

Jul 3Podcast

Why Nitro Is More Than a Faster Bridge

Marc Rousavy (Margelo) talks Nitro modules, native state, zero-copy data, Vision Camera, and why speed is only part of the story.

cover

May 25Article

Profile React Components in React Native Release Builds

Learn how to profile React components in React Native release builds with @callstack/inspector, using an Expo app setup that requires zero native code changes.

cover

Nov 21Article

Memory Ownership Models: When JavaScript Meets Native Code

A deep dive into how React Native handles memory when JavaScript and native code share ArrayBuffer data. Learn why async processing can break, how GC affects raw pointers, and the safe patterns for building high-performance native integrations.

cover

Oct 14Article

From Files to Buffers: Building Real-Time Audio Pipelines in React Native

Learn how to move beyond file-based audio APIs and work directly with in-memory PCM data in React Native. This guide shows how to construct WAV buffers, use JSI for zero-copy transfers, and enable real-time audio processing.

cover

Sep 8Article

How to Add Custom JSI Bindings in Your TurboModule (and Why You Might Need Them)

TurboModules provide a type-safe way to write native code in React Native. JSI sits underneath as a lower-level API, enabling direct interaction with the JavaScript runtime. Beyond what TurboModules expose, JSI gives you access to powerful features, such as working directly with ArrayBuffers. This article shows how to inject a custom binding step by step.

cover

Jul 24Article

Exploring Fibers: Cooperative Multitasking and Lock-Free Task Execution

Explore how fibers enable lightweight, cooperative multitasking. This guide walks through building a simple scheduler, managing task dependencies without locks, and using Tracy to visualize performance.

cover

May 21Article

Multithreading Isn’t Free: Performance Pitfalls Visualized

Learn how to profile and optimize multithreaded apps. Discover why more threads don’t always mean faster results and how bad thread contention really is.

Event Cover

Apr 28Webinar

Master JavaScript Performance in React Native (Part 2)

Learn to fix JS performance issues in React Native-from debounce bugs to Redux selectors, in this hands-on webinar with real code examples.

Events Cover

Apr 24Talk

Tips & Tricks From The Ultimate Guide to React Native Optimization 2025

View flattening, React 19 hooks, real DevTools tips-learn what actually helps your React Native app run faster.

Events Cover

Apr 24Talk

User-Focused Performance Tracking

Discover how Expensify defines and tracks performance metrics that actually impact user experience.

cover

Dec 18Podcast

React Native FastIO: Nitro-Powered Performance Boost

Listen to Mike Grabowski talk about React Native FastIO, a collection of primitives for working with data in React Native and its performance benefits.

cover

Oct 15Podcast

Superfast Nitro Modules With Marc Rousavy

Even faster React Native apps? That's possible with Nitro Modules! Tune in to the conversation with their creator, Marc Rousavy.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.