Detaching Expo Apps to ExpoKit

In my Easier React Native Development With Expo post, you learned about how Expo makes it easier for beginners to begin creating apps with React Native. You also learned that Expo allows developers to get up and running with developing React Native apps faster because there's no longer a need to set up Android Studio, Xcode, or other development tools.

But as you have also seen, Expo doesn't support all of the native features that an app might need. Though the Expo team is always working to support more native functionality, it's a good idea to learn how to convert an existing Expo project to a standard native project so you can easily transition if the need arises. 

So, in this two-part series, we're taking a look at how to do that. In the first part of the series, you learned the basic concepts of ExpoKit. In this post, we'll continue where we left off by actually detaching the app to ExpoKit and continue coding the location-sharing app. 

Detaching to ExpoKit

In order to detach to ExpoKit, you first have to edit the app.json and package.json files. 

In the app.json file, make sure that a name has been set. The platforms should be the platforms you want to build to.

If you want to build for iOS, you must specify the ios option:

If you want to support Android, then also specify the following option:

There are other options that were prefilled by the exp command-line tool when the project was created. But the only important ones are the bundleIdentifier for iOS and package for Android. These will be the unique IDs for the app once they get published to the Apple or Play store. Detaching requires those details because it actually generates the native code for the app to be run on a device. You can find more information about the different configuration options for the app.json file in the Expo documentation.

You can view the full contents of the file in the GitHub repo.

Next, open the package.json file and add the name of the project:

This should be the name that you used when you created the project using exp init. It's very important that they are the same because the name you specify in the package.json is used when compiling the app. Inconsistencies in this name will cause an error.

Now we're ready to detach to ExpoKit. Execute the following command at the root of the project directory:

This will download the native Expo packages for Android and iOS locally.

You should see an output similar to the following if it succeeded:

Expo detach

If you're deploying to iOS, you need to install the latest version of Xcode. At the time of writing of this tutorial, the latest version is 9. Next, install CocoaPods by executing sudo gem install cocoapods. This allows you to install the native iOS dependencies of the project. Once that's done, navigate to the ios directory of the project and execute pod install to install all the native dependencies. 

Installing Custom Native Packages

Now that we have detached, we can now install native packages just like in a standard React Native project. 

For this app, we'll need the React Native Background Timer and Pusher packages.

First, install the Pusher package because it's easier:

This allows us to communicate with the Pusher app you created earlier.

Next, install the React Native Background Timer. This allows us to periodically execute code (even when the app is in the background) based on a specific interval:

Unlike the Pusher package, this requires a native library (either iOS or Android) to be linked to the app. Executing the following command does that for you:

Once it's done, it should also initialize the module on android/app/src/main/host/exp/exponent/ But just to make sure, check if the following exists in that file:

If you're building for iOS, open the Podfile inside the ios directory and make sure the following is added before the post_install declaration:

Once that's done, execute pod install inside the ios directory to install the native module.

For Android, this is already done automatically when you run the app using Android Studio.

Update the Android Manifest File

If you're building for Android, open the Android manifest file (android/app/src/main/AndroidManifest.xml) and make sure the following permissions are added:

This allows us to ask permission for Pusher to access the internet and Expo to get the user's current location on Android devices. 

Running the App

We're not yet done, but it's better to run the app now so you can already see if it works or not. That way, you can also see the changes while we're developing the app.

The first step in running the app is to execute exp start from the root directory of the project. This will start the development server so that any change you make to the source code will get reflected in the app preview.

If you're building for Android, open Android Studio and select Open an existing Android Studio project. In the directory selector that shows up, select the android folder inside the Expo project. Once you've selected the folder, it should index the files in that folder. At that point, you should now be able to rebuild the project by selecting Build > Rebuild Project from the top menu. Once that's done, run the app by selecting Run > Run 'app'.

Android Studio can run the app on any Android device connected to your computer, on one of the emulators you installed via Android Studio, or via Genymotion (Android Studio automatically detects a running emulator instance). For this app, I recommend you use Genymotion emulator since it has a nice GPS emulation widget that allows you to change the location via a Google Maps interface:

Genymotion location emulation

(If you're having problems running the app on your device, be sure to check out this Stack Overflow question on getting Android Studio to recognize your device.)

Once that's done, open the ios/ocdmom.xcworkspace file with Xcode. Once Xcode is done indexing the files, you should be able to hit that big play button and it will automatically run the app on your selected iOS simulator.

Xcode also allows you to mock the location, but only when you build the app for running in the simulator. Making a change to the code and having the development server refresh the app won't actually change the location. To change the location, click on the send icon and select the location you want to use:

Xcode location simulation

Continue Coding the App

Now we're ready to continue writing the code for the app. This time, we'll be adding the functionality to run some code while the app is in the background.

Adding a Background Task

Import the Pusher and Background Timer package that you installed earlier:

Set the value for the Google API key of the Google project you created earlier:

Use the Location and Permissions API from Expo:

Expo's APIs work cross-platform—this is not unlike a standard React Native project where you have to install a package like React Native Permissions to gain access to a permissions API that works cross-platform.

Next, set the interval (in milliseconds) that the code for tracking the user's current location is going to execute. In this case, we want it to execute every 30 minutes. Note that in the code below we're using the value of the location_status variable to check whether the permission to access the user's current location was granted or not. We'll be setting the value of this variable later, once the component is mounted:

Getting the Current Location

Get the current location by using Expo's Location API:

Next, using the Google Maps Geocoding API, make a request to the reverse geocoding endpoint by supplying the latitude and longitude values. This returns a formatted address based on those coordinates:

Sending the Location With Pusher

The next step is to send the location using Pusher. Later on, we're going to create the server which will serve as the auth endpoint and at the same time display the page which shows the user's current location.

Update the constructor to set a default value for the Pusher instance:

When the component is mounted, we want to initialize Pusher. You can now supply the Pusher API key and cluster from the setting of the Pusher app you created earlier:

Next, you can now add the code for sending the current location. In Pusher, this is done by calling the trigger() method. The first argument is the name of the event being triggered, and the second argument is an object containing the data you want to send. 

Later on, in the server, we'll subscribe to the same channel which we will subscribe to once the component is mounted. Then we'll bind to the client-location event so that every time it's triggered from somewhere, the server will also get notified (although only when the page it's serving is also subscribed to the same channel):

The only time we're going to ask for permission to access the user's current location is when the component is mounted. We will then update the location_status based on the user's selection. The value can either be "granted" or "denied". 

Remember that the code for checking the user's current location is executed periodically. This means that the new value of the location_status variable will also be used at a later time when the function is executed. After that, we also want to subscribe to the Pusher channel where the location updates will be sent:

Creating the Server

Now we're ready to create the server. First, create your working directory (ocdmom-server) outside of the project directory of the app. Navigate inside that directory and execute npm init. Just press Enter until it creates the package.json file.

Next, install the packages that we need:

Here's an overview of what each package does:

  • express: used for creating a server. This is responsible for serving the tracking page as well as responding to the auth endpoint.
  • body-parser: Express middleware which parses the request body and makes it available as a JavaScript object. 
  • pusher: used for communicating with the Pusher app you created earlier.

Once that's done, your package.json file should now look like this:

Create a server.js file and import the packages we just installed:

Configure the server to use the body-parser package and set the public folder as the static files directory:

Initialize Pusher. The values supplied here will come from the environment variables. We will add those later, when we deploy the server:

Serve the tracking page when the base URL is accessed:

Next, create the route for responding to requests to the auth endpoint. This will be hit every time the app initializes the connection to Pusher, as well as when the tracking page is accessed. What this does is authenticate the user so they can communicate to the Pusher app directly from the client side. 

Note that this doesn't really have any security measures in place. This means anyone can just make a request to your auth endpoint if they have access to your Pusher App key. In a production app, you'd want more robust security!

Lastly, make the server listen to the port specified in the environment variables. By default, it's port 80, but we're also setting it as an alternate value just in case it doesn't exist:

Tracking Page

The tracking page displays a map which gets updated every time the client-location event is triggered from the app. Don't forget to supply your Google API key:

Next, create a public/js/tracker.js file and add the following:

The function above extracts the query parameter from the URL. The unique code (the one displayed in the app) needs to be included as a query parameter when the base URL of the server is accessed on a browser. This allows us to keep track of the user's location because it will subscribe us to the same channel as the one subscribed to by the app.

Next, initialize Pusher. The code is similar to the code in the server earlier. The only difference is that we only need to specify the Pusher app key, auth endpoint, and cluster:

Check if the code is supplied as a query parameter, and only subscribe to the Pusher channel if it's supplied:

Add the function for initializing the map. This will display the map along with a marker pointing to the default location we've specified:

Bind to the client-location event. The callback function gets executed every time the app triggers a client-location event which has the same unique code as the one the user supplied as a query parameter:

Next, add the styles for the tracking page (public/css/style.css):

Deploying the Server

We'll be using Now to deploy the server. It's free for open-source projects.

Install Now globally:

Once it's installed, you can now add the Pusher app credentials as secrets. As mentioned earlier, Now is free for open-source projects. This means that once the server has been deployed, its source code will be available at the /_src path. This isn't really good because everyone can also see your Pusher app credentials. So what we'll do is add them as a secret so that they can be accessed as an environment variable. 

Remember the process.env.APP_ID or process.env.APP_KEY from the server code earlier? Those are being set as environment variables via secrets. pusher_app_id is the name assigned to the secret, and YOUR_PUSHER_APP_ID is the ID of your Pusher app. Execute the following commands to add your Pusher app credentials as secrets:

Once you've added those, you can now deploy the server. APP_ID is the name of the environment variable, and pusher_app_id is the name of the secret you want to access:

This is how it looks once it's done deploying. The URL it returns is the base URL of the server:

deploy server

Copy that URL over to the App.js file and save the changes:

At this point, the app should now be fully functional.


That's it! In this two-part series, you've learned how to detach an existing Expo project to ExpoKit. ExpoKit is a good way to use some of the tools that the Expo platform provides while your app is already converted to a standard native project. This allows you to use existing native modules for React Native and to create your own. 

While you're here, check out some of our other posts on React Native app development!