Build an NPM Package with Expo Dependencies for Expo and Plain React Native Apps

This guide will cover how to publish a react-native-compatible package to NPM, especially in the case that your package depends on expo-* libraries (e.g. expo-notifications or expo-media-library). These libraries can be used in plain react-native projects as well, but it’s not so clear how to include them in an NPM package so that they play nicely with both expo projects and plain react-native projects.

Sections

Introduction

The Expo project includes some very powerful libraries as well as its own collection of development tools which can be used to build react-native apps faster and without needing to manage separate android and ios folders, as one does in a traditional react-native app. The powerful expo-* libraries can be used in traditional react-native apps as well as apps powered by Expo, but the process for including such libraries is different for each.

This can lead to some confusion about how to package and publish NPM packages which depend on expo-* libraries. Additionally, it’s not always clear how to make such packages work in both expo-powered apps and traditional react-native apps (these are respectively called “managed” and “bare” projects in the Expo documentation).

But there is a simple way to package, build, and publish modules to NPM while being able to test and ensure that these modules work with all types of react-native apps, and that’s what we’ll dive into here.

Things to Remember

If you’ve already published NPM packages before, and you’re just wondering about the differences when expo/react-native is involved, here are some things to keep in mind:

  1. Don’t include react or react-native as dependencies. Include them as peerDependencies instead so users of your package don’t end up having two different versions of react or some other important package which don’t play well together
  2. You don’t need to include expo as any kind of dependency at all.
  3. If you are transpiling your module code from typescript you need react-native-unimodules in your devDependencies, do not install @unimodules/core
  4. Users of your package with plain react-native apps need to install react-native-unimodules and follow the additional instructions as well. Point this out in your package’s installation instructions.
  5. Some expo-* libraries have extra installation instructions for plain react-native projects (e.g. expo-media-library). Point this out in your package’s installation instructions if so.

With that in mind, let’s begin!

Writing your package

We’re going to make a very basic NPM package called rn-barometer which exports a react-native component called AirPressure that displays the current Air Pressure in Pascals as detected by the device. This package will depend on expo-sensors and we’re going to make this package work on both expo-powered apps and plain react-native apps.

First you need to install any needed devDependencies and regular dependencies:

Although our package needs react and react-native and react-native-unimodules to work, they cannot be specified as regular dependencies of our package. See Two Reacts Won’t Be Friends for more on why this is.
yarn add -D typescript @types/react @types/react-native react-native-unimodules
yarn add expo-sensors

Now, we create our package. The code for the package is below:

src/index.tsx

import React, { useState, useEffect } from 'react';
import { Barometer, BarometerMeasurement } from 'expo-sensors';
import { StyleSheet, Text, View } from 'react-native';

export function AirPressure()  {
  const emptyMeasurement: BarometerMeasurement = {pressure: 0, relativeAltitude: 0};
  const [data, setData] = useState(emptyMeasurement);
  let subscription: any = null;

  useEffect(() => {
    _subscribe();
    return _unsubscribe;
  });

  const _subscribe = () => {
    subscription = Barometer.addListener(barometerData => {
      setData(barometerData);
    });
  };

  const _unsubscribe = () => {
    subscription && subscription.remove() && Barometer.removeAllListeners();
    subscription = null;
  };

  const { pressure = 0 } = data;

  return (
    <View style={styles.container}>
      <Text>{pressure * 100} Pa</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  }
});

You’ll also need a tsconfig.json and a package.json. Here are ours so far.

tsconfig.json

{
  "compilerOptions": {
    "declaration": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "jsx": "react",
    "lib": ["es6"],
    "moduleResolution": "node",
    "strict": true,
    "target": "esnext",
    "sourceMap": true,
    "outDir": "dist",
    "rootDir": "src",
  },
  "include": ["src"],
  "exclude": [
    "node_modules"
  ]
}

package.json

{
  "name": "rn-barometer",
  "version": "1.0.0",
  "description": "An AirPressure component for react-native that displays the current air pressure",
  "keywords": [
    "react-native",
    "expo",
    "barometer",
    "air pressure",
    "rn-barometer"
  ],
  "author": "Farhan Kathawala <[email protected]>",
  "license": "MIT",
  "devDependencies": {
    "@types/react": "^16.9.56",
    "@types/react-native": "^0.63.35",
    "react-native-unimodules": "^0.11.0",
    "typescript": "^4.0.5"
  },
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  },
  "dependencies": {
    "expo-sensors": "^9.1.0"
  }
}

Building your package

Our package needs to be transpiled from typescript to javascript.

For this reason we’ll add the following lines to our package.json

{
  "scripts": {
    "build": "tsc",
    "prepare": "yarn run build"
  },
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist/**/*"
  ],
}

When tsc runs, it will create a dist/ folder that contains transpiled .js files. Those are the files which will end up being used by whoever downloads our NPM package. And so we say in our package.json that the main file is dist/index.js and the associated type declaration file for it is dist/index.d.ts.

You can build your package now by running the following.

yarn run build

Testing our package

Now that our package is built, it’s time to test that it works in both Expo and plain react-native projects.

Expo project

Initialize a quick expo project the following way (replace path/to/lib with the full path to our rn-barometer library)

When running yarn add on a local module, the whole node_modules folder of the local module gets pulled in. This means, before pulling in that local module, you might want to clear node_modules in the local module and run yarn install --production so that your devDependencies (i.e. react-native-unimodules) are not pulled in.
cd ~/
expo init expo-rn-barometer -t expo-template-blank-typescript
cd expo-rn-barometer
yarn add path/to/lib/rn-barometer
expo start

Now add the AirPressure component to your expo project

App.tsx

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { AirPressure } from 'rn-barometer';

export default function App() {
  return (
    <View style={styles.container}>
      <StatusBar style="auto" />
      <AirPressure/>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Now you should be able to test the app on your device / emulator. Eventually you should see the following:

expo app with barometer reading

Plain react-native project

Before anyone with a plain react-native app can use your NPM package they MUST install react-native-unimodules and go through the steps listed here. Please make this clear in your package’s README

We’ll initialize our plain react-native app very simply

npx react-native init plainrnbarometer --template react-native-template-typescript
cd plainrnbarometer

Now, we need to install react-native-unimodules

yarn add react-native-unimodules

And we need to follow the additional installation directions here

Once that is finished, we’ll have a plain react-native project which can use expo dependencies.

Now just

When running yarn add on a local module, the whole node_modules folder of the local module gets pulled in. This means, before pulling in that local module, you might want to clear node_modules in the local module and run yarn install --production so that your devDependencies (i.e. react and react-native) are not pulled in.
yarn add path/to/lib/rn-barometer

And add the AirPressure component like before

App.js

import React from 'react';
import {StyleSheet, View, StatusBar} from 'react-native';

import {AirPressure} from 'rn-barometer';

const App = () => {
  return (
    <View style={styles.container}>
      <StatusBar barStyle="dark-content" />
      <AirPressure />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

export default App;

Now, build the app.

Before building your app, run yarn react-native doctor and make sure you are not seeing any errors or warnings. Often times SDK or build tool mismatches will lead to your build failing, but this command will let you know beforehand if any of your tools are out of sync or you have the wrong version of some SDK installed.
yarn react-native start &
yarn react-native run-android

If all went well you should see something like this

plain react-native app with barometer reading

Publishing

Now the fun part!

Publish your package to NPM with

yarn publish

And answer any prompts which arise.

You may not see your package on npmjs.com immediately, but you can see the status of your package in the NPM registry with the following command

npm view expo-file-dl

It’s also a good idea to include a README page with your package that details the different installation process for expo-powered apps vs. plain react-native apps. The main difference is that plain react-native apps need to install their own copy of react-native-unimodules and follow its post-installation steps. Those apps may also need to follow additional post-installation steps after adding your module if your module depends on certain expo-* libraries (e.g. expo-media-libarary).

Conclusion

That’s it! I figured this process out while publishing a recent package to NPM, called expo-file-dl, which lets you download a file to the public folders on a mobile device and shows a download-progress notification to the user. Hope it helps out someone out there :)

Farhan Kathawala
Farhan Kathawala
Full Stack Web / React Native Developer

A happy full stack developer sharing tidbits of everything and anything web / mobile app dev.

Related