Expo is a great delivery system for getting React Native mobile apps off the ground and into the hands of users. If I were to do it all over again, here is what I would have loved to know ahead of time!
Standalone App = A native app file built by Expo for iOS (ipa) or Android (apk) that is ready to upload to the respective app stores.
App Store = Either Apple’s App Store or Google Play for Android
Publish = Ah this does not mean publishing to the app stores. This means an over-the-air update. This term is accurate for Expo because your app becomes immediately available to use for users in the Expo App Client itself.
OTA = Over The Air, meaning a live update of your app content and functionality via the internets. Like serving a webpage.
Build = Means a process by which Expo uploads your code to its servers (publish) and then runs a build process in the Cloud that gives you a link to download the final product.
Your Expo Account = Your App Ecosystem
An Expo Account is *the* account that stores and serves your app. It is not a user account! Publishing your apps under this account essentially locks it to the username, and trying to switch to a different account later can cause problems. For example I identified that switching your account and republishing the same app to the same release channel will cause all the previously saved AsyncStorage user data (iOS UserDefaults) to be inaccessible.
Also if you do get into a bind of transitioning off to a new Expo account, you will end up having to annoyingly maintain apps on an old Expo login and duplicate efforts to publish your app. This is because Accounts essentially own the ecosystem which is explained later.
SecureStore vs AsyncStorage
The solution to the above problem is to use SecureStore to persist values in the user’s phone keychain. This even persists data after the user has deleted their app. SecureStore does have character limits though and I wouldn’t trust it to deal with large amounts of object data, so it should only be important key-value pairs. AsyncStorage is still important for persisting app session state, but essentially it should be treated as if the data could be wiped tomorrow.
Use Release Channels
Do it now or feel the pain later. The hierarchy of Expo is this:
- Expo Account
- Expo App
- Release Channel
- iOS/Android App
- Release Channel
- Expo App
As noted above, if you ever have to switch accounts, you will end up maintaining this entire ecosystem TWICE. Your “production” release channel is different between each account!
I recommend creating new release channels for each standalone app version update too. This is because Expo often comes out with new and awesome SDK version upgrades which require a new build. The new SDK version might have code changes or features that either are not compatible or do not exist in the previous versions. A new build is also required when adding new app permissions, loading screen stuff, or tablet support.
And I won’t get into it, but you’ll see how even more complicated things get if you decide to splinter iOS and Android into separate release channels.
Just Let Expo Update the App
Expo provides an option to disable Over-the-Air updates (OTA) and provides a nice API to administer Updates yourself. This seems easy at first but can get you in trouble later.
- Expo has had issues whereby the notification of a new Update could continue to flag even after the user has updated, creating an endless reload loop.
- Finding a way to lock the user down to an updated version has lots of pitfalls. What if the very code to determine if the user needs to download the Update is itself broken? You have bricked their app!
- Publish rollbacks do not resolve the above problem because you have already told Expo not to automatically update the app. It has essentially updated itself into oblivion!
- App.json settings can not be rolled back to automatic updates because it is already integrated into the build that is live in the App Store.
- Expo always shows its own error pages, overriding your componentDidCatch. So you couldn’t even provide an escape route or user prompt for a bricked app.
Don’t worry about it! Are updates really that big? If you’re pushing minor updates now and then the download time is minimal. And if you’re doing massive code changes, you’re better off just re-building the app for store publishing. Plus your app is basically an Expo wrapper to begin with, so you’re not really saving that much time for the convenience.
App Store Updates are Still Important
Firstly, app store updates are also meaningless. Assuming you didn’t generally tweak the app.json, you could push out app store versions 1.1, 1.2, and 1.3 and they could all read a single “production” release channel. A single publish would push to all users regardless of version. In Expo, the app.json “version” is your arbitrary versioning scheme, only “sdkVersion” is critical. Meaning it’s marketing fluff for users, much like how Google Play uses versionName vs versionCode.
I already noted some use cases by which an app store update is required. However, the magic really comes when we talk about OTA updates that fail. If an OTA update fails, meaning Expo can’t get the newest hottest thing you published, it will automatically fallback to its “bundled” state. This means Expo will serve from cache the app at the time you built it.
When disabling OTA updates and having bundled assets, you basically mimic typical native app development where each version update is a snapshot build. With OTA updates enabled, you still want an emergency cushion if Expo can’t serve the newest stuff. Under the hood, Expo will continue to try and update the user and they should see new stuff on the next app open.
Expo and React Native have definitely solved the problem of fast development and relative ease of delivery. However long-term maintenance and deployment of live code is still an issue. There is for example no UI by which one can easily track release channels or which users are seeing them, you really have to memorize them and provide good analytics reporting. The Expo website is a pretty basic listing of your apps and recent builds.
Overall if your team is investing in React Native over native, this is the platform to go with but do maintain robust internal tracking of your releases.