Adding an Example App to Your React Native Library

You made a shiny new library for React Native and now it’s time to show it to the world. But you need to add an example app, so people can try it out before installing. Having an example also makes it easy to test changes as you develop. This seems like such a simple thing, doesn’t it?

Unfortunately it’s not that simple and can be quite challenging. I’ll describe the process I use and maybe it’ll help you out a bit. Note that I’ll cover JS only libraries. If your library has native code, then this article will still be useful, but you’ll need a separate step for hooking up the native code to the app.

So let’s get started.

Why is it hard?

Before exploring solutions, let’s discuss why it’s hard. React Native uses a custom packager called Metro Bundler for bundling your JavaScript code and a module resolution mechanism called haste.

Metro is quite fast and great for large codebases. But right now it has few limitations, which makes it a little hard to use for an example:

  • We can’t use symlinks, since watchman, the file watcher used by Metro Bundler, doesn’t support it.
  • We cannot import modules outside of the current directory.
  • It produces warnings (previously errors) when it encounters two modules with the same name, which is easy to run into in our case since we’ll have same modules installed for development and for the example.

But you know what? It’s actually possible to configure the packager so that we can work around the limitations for our example app. However the configuration options are not documented yet, so it can be hard to configure unless you are familiar with it.

If you have ideas on how to solve these problems, please open an issue for discussion or open a PR in the Metro Bundler repository :)

How do we do it?

Here, I’ll assume that you are using Expo for your example. Why Expo? Because it’s super easy for you to develop and it’s unbelievably easy for people to check the example out. But I’ll also include notes for people who don’t want to/can’t use Expo (in case your library requires native code).

Before starting, here’s the directory structure I use. You can use a different one, but you’ll need to tweak the guide for your needs which can be challenging. So I recommend to stick to the following directory structure.

├── example
├── package.json
└── src
└── index.js

Our library exists under the src/ folder, whereas the example app (which is a full Expo or React Native app) exists under the example/ folder.

Let’s go ahead and initialize a new example app with exp init example, if you want to use Expo or react-native init example, if you want to go with vanilla React Native.

Configuring Babel

We’d need to add our library as a dependency to the example app. My naive approach was to add the library as a file: dependency, which more or less works. But when you change things, you always have to remember to reinstall, which results in a poor DX. Furthermore, npm@5 installs file: dependencies as symlinks, which breaks with React Native Packager.

What if we could alias the library instead, like we do with Webpack? That’s what babel-plugin-module-resolver does!

First, install the plugin.

yarn add --dev babel-plugin-module-resolver

Now, let’s create a .babelrc file (or edit it if it already exists), and make sure it looks like this:

{
"presets": [
"expo"
],
"plugins": [
["module-resolver", {
"alias": {
"name-of-my-library": ".."
}
}]
]
}
view raw .babelrc hosted with ❤ by GitHub

Note: If your example is a React Native project and not an Expo project, make sure it’s react-native instead of expo in the presets field.

Remember to replace name-of-my-library with the name of your library which you’ll use to import the library throughout the example. This is optional and you can always import it like ../../, but it’s inconvenient, and makes it impossible to directly copy-paste code-snippets from the example into a project.

If you are planning to use react-native-vector-icons in your library and using Expo for the example app, your .babelrc should look like this, since you’ll need to duplicate expo’s config.

{
"presets": [
"expo"
],
"plugins": [
["module-resolver", {
"alias": {
"name-of-my-library": "..",
"react-native-vector-icons": "@expo/vector-icons"
}
}]
]
}
view raw .babelrc hosted with ❤ by GitHub

Now we are done with configuring babel, but we still need to configure Metro Bundler.

Configuring Metro Bundler

Packager uses special configuration file which is usually named metro.config.js (rn-cli.config.js in older versions). Let’s create that file in our example folder and paste the following:

const path = require('path');
module.exports = {
projectRoot: __dirname,
watchFolders: [path.resolve(__dirname, '..')],
};
view raw metro.config.js hosted with ❤ by GitHub

This tells the packager to set the project root to current directory (which is the example folder) and watch the parent directory. This allows us to import files from the parent directory.

Unfortunately, the imports for node modules will fail for the files in the parent directory, i.e. the directory where the source code for our library exists. So we need to duplicate the dependencies in the example folder.

Say we have lodash and prop-types modules as dependencies of the library, we now need to install them in the example folder.

yarn add lodash prop-types

Now, we’ll need to configure metro’s resolver so it can find these dependencies. To do that, let’s add a new resolver key in the config. Under the resolver key, we need to add providesModuleNodeModulesproperty which lists all of the imported modules in the library (both direct dependencies and peer dependencies). It’ll look something like this:

const path = require('path');
module.exports = {
projectRoot: __dirname,
watchFolders: [path.resolve(__dirname, '..')],
resolver: {
providesModuleNodeModules: [
'react-native',
'react',
'prop-types',
'lodash',
],
},
};
view raw gistfile1.txt hosted with ❤ by GitHub

We’ll also need to blacklist the parent node_modules directory, otherwise packager is going to complain about duplicate modules. Let’s install escape-string-regexp package and then update the config.

yarn add --dev escape-string-regexp

Now, let’s update our config to add a blacklistRE property under resolver, which should look like this:

const path = require('path');
const blacklist = require('metro-config/src/defaults/blacklist');
module.exports = {
projectRoot: __dirname,
watchFolders: [path.resolve(__dirname, '..')],
resolver: {
blacklistRE: blacklist([
new RegExp(
`^${escape(path.resolve(__dirname, '..', 'node_modules'))}\/.*$`
),
]),
providesModuleNodeModules: [
'react-native',
'react',
'prop-types',
'lodash',
],
},
};
view raw gistfile1.txt hosted with ❤ by GitHub

If you’re using an older version of Metro (for example React Native ≤ 0.55), it’ll look like this instead:

const path = require('path');
const blacklist = require('metro/src/blacklist');
module.exports = {
getProjectRoots() {
return [__dirname, path.resolve(__dirname, '..')];
},
getProvidesModuleNodeModules() {
return [
'react-native',
'react',
'prop-types',
'lodash',
];
},
getBlacklistRE() {
return blacklist([
new RegExp(
`^${escape(path.resolve(__dirname, '..', 'node_modules'))}\/.*$`
),
]);
},
};
view raw gistfile1.txt hosted with ❤ by GitHub

Note that, if you have other node_modules folders somewhere in the parent directory (say a docs folder with it own node_modules), you might have to add it to the blacklist too.

Now we are done with the config for packager. We just need to make React Native use it. If you are using expo, add the following keys under packagerOpts key in exp.json.

"packagerOpts": {
"config": "./metro.config.js",
"projectRoots": ""
}
view raw gistfile1.txt hosted with ❤ by GitHub

Note: We need to specify projectRoots as an empty string to override the default value set by Expo.

If you are not using Expo, you can pass the config with the--configparameter in the React Native CLI.

Wrapping up

Now you should be able to run exp start or yarn start (for non-expo projects) inside the example folder and everything should work as expected. You should be able to change files under src/ and reload the example app to see changes instantly.

You can checkout the react-native-tab-view project for an example of the example!

Was the article useful? If you find it confusing, want more explanation, or just want to give some general feedback or suggestions, contact us.

Insights

Learn more about Developer Experience

cover

May 5Article

Codex Ambassador: Q1 Recap Across 5 Cities

A short recap of Codex events in Berlin, Wroclaw, Warsaw, Sopot, and Miami, plus what we learned from each format and where we want to go next.

cover

Feb 5Live Stream

Storybook Explained: From Basics to Advanced Workflows

Core Storybook concepts, stories and args, interaction testing, and how Storybook works in React Native projects.

cover

Jan 29Article

Announcing AI SDK Profiler for React Native

Announcing the AI SDK Profiler plugin for Rozenite DevTools. Capture OpenTelemetry spans from the Vercel AI SDK and inspect AI requests across on-device and cloud providers.

cover

Nov 28Talk

How I Hacked React Native DevTools (And Built a Plugin Framework)

Learn how React Native DevTools work under the hood, and how to inject custom plugins with clean hooks and no forks.

cover

Sep 16Talk

How I Hacked React Native DevTools (And Built a Plugin Framework)

Learn how React Native DevTools work under the hood, and how to inject custom plugins with clean hooks and no forks.

cover

Jul 28Article

Introducing Rozenite: A Plugin Framework for React Native DevTools

Build custom debugging panels inside React Native DevTools with Rozenite: a modular, TypeScript-ready plugin framework with full device-scoped support.

cover

Feb 12Article

How to Easily Create Native Libraries With Nitro and Turbo Modules

Learn how to optimize React Native performance with Turbo and Nitro Modules. Ditch the legacy bridge and master modern native module development.

cover

Dec 4Podcast

Radon IDE: Better Developer Experience for React Native Teams

Find out how Radon IDE can help increase team velocity and rid you of the common React Native app development pain points.

cover

Sep 19Podcast

Exploring Bun: Jarred Sumner on Hyperfast JavaScript Toolkit

Is Bun the future of React Native package management? Join us as we talk with Jarred Sumner, the creator of this lightning-fast JavaScript toolkit.

cover

Sep 11Podcast

Vim: from Zero to Hero

Discover the power of Vim, the legendary text editor, as we explore its origins, standout features, and practical tips to boost your coding efficiency.

cover

May 15Article

Add-Ons for Web Browsers With React

At times you may need to create an add-on to use a web browser more effectively. Find out how to build one with React.

cover

Jan 27Article

Why Is It Worth to Automate Your Dependency Management with `Autolinking`?

When you automate your dependency management, you can fully focus on developing your app. Find out how you can do it with autolinking.

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.