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
- Things to Remember
- Writing your package
- Building your package
- Testing our package
- Publishing
- Conclusion
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:
- Don’t include
react
orreact-native
as dependencies. Include them as peerDependencies instead so users of your package don’t end up having two different versions ofreact
or some other important package which don’t play well together - You don’t need to include
expo
as any kind of dependency at all. - If you are transpiling your module code from typescript you need
react-native-unimodules
in your devDependencies, do not install@unimodules/core
- 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. - 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:
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)
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:
Plain react-native project
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
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.
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
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 :)