Table of Contents
- React Native Expo Template
This template is a starting point for building a React Native app using Expo in STRV.
It provides a foundation for every stage of the development process:
🔧 For the project start and initial development:
- Expo Development build setup
- domain driven folder structure
- Storage service using MMKV
- Font and size scaling utilities
📏 For keeping the code quality high and enforcing code standards:
- basic unit test setup
- Linter and formatter setup
- pre push and pre commit hooks
🚀 For shipping and production maintenance:
- Github Actions for CI/CD
- App config ready for different environments
- Utilities for version check and forced updates in production
⚠️ We don't include any more opinionated solutions such as:
- Styling and theming solution
- State management library
- Image loading library
- E2E testing setup You need to decide what is best for your project and add it to the project. But we provide some recommendations in the Other Recommended Solutions section.
- Clone the repository
- Run
pnpm install
to install dependencies - copy
.env.example
to.env
usingcp .env.example .env
- run
pnpm ios/android
to start the development server and run the app on your device
To use the full CI/CD pipeline you also need to:
- Setup EAS and EAS credentials
- Setup Github environment
- Setup Slack app for notifications (optional)
- Jira Setup (optional)
- The main benefit is better maintainability as most of the native setup is done through
app.config.ts
and community/custom config plugins. Updating Expo SDK mostly assures compatibility with majority of dependencies used, which is a common source of problem when upgrading React Native separately. You can use expo doctor to check for any issues with the setup and update the project accordingly.
- EAS helps with building and app submission. It can create and store all important credentials so that we don't have to distribute them among everyone.
- Default build profiles in
eas.json
:dev
- this profile will build anexpo-dev-client
, meaning that after installing the app, one can change non-native code and see changes reflected in the appstaging
- should be distributed for testing, does not have a dev client, meaning it cannot be manipulated. It buildscom.xxx.xxx.staging
application which can be distributed through a link or a QR code.
For iOS, we need to add devices for EAS testing (development, staging) via
eas device:create
(creates sharable registration link) and confirm the device has been added. It is good to write down your unique device ID to understand what is your device.
production
- non-distributable build that should be submitted to Play Store and App Store. Can be tested through Play Store internal testing track or Testflight. It builds the officialcom.xxx.xxx
package later released to production.
- Environment variables are managed by Expo, use
EXPO_PUBLIC
prefix to make them accessible in the app app.config.ts
determines based on the environment a relevant icon, app name and appIdentifier to distinguish individual apps and allow installing side by sideeas.json
can setAPP_ENV
variable for each build profile to define environment
babel-plugin-transform-remove-console
to remove console logs in production
Dev plugins included in the template:
- React Navigation - to see navigation state, history, and params passed to screens
- MMKV - to see MMKV store
Not included but useful:
- We are using @strv/eslint-config-react-native config which is an extension of Expo Universe config with couple of extra rule changes we have found useful.
- We are using mostly standard prettier config.
- Husky is used to run linting and formatting before committing and pushing code.
- Lint Staged is used to run linting and formatting before committing.
User persistence is setup through MMKV
which is a synchronized and faster alternative to AsyncStorage
.
expo-updates
is used to deliver and check for new updates. useOTAUpdates()
checks if the update is available on mount and whenever the app goes from background to foreground via useOnForeground()
hook. If an update is available, we should show to the user screen/modal/alert that would suggest them to update (reload the app).
⚠️ The OTA updates are used for patches to javascript layer only which is convenient for small bug fixes and UI changes.
To deliver the update through eas update
we need to target the right channel
from eas.json
and manage runtimeVersion
in app.config.ts
for native layer compatibility otherwise we risk updating incompatible environment resulting in app crashes.
⚠️ runtimeVersion should be changed whenever we change native layer, meaning a new native third party dependency is installed in package.json or we do native config changes in app.config.ts.
❓ A versioning strategy could be to bump minor version for every native change and bump patch version for every javascript change. Then
runtimeVersion
would change from0.1.0
to0.2.0
, whileversion
could change as follows:0.1.0
>0.1.x
>0.2.0
. This meansruntimeVersion
0.1.0
is compatible with allversions
0.1.x
.
⚠️ if we runeas update
locally, current.env
file is used, so be careful not to publish to production development variables. Better to do it through a github action and setup environment variables as Github Secrets.
Before going to production we should have a forced update functionality in place for cases such as when we introduce a major bug or our backend API is not backward compatible. useStoreUpdate()
hook compares minimumSupportedVersion
or recommendedVersion
with installedAppVersion
from app.config.ts
. The minimum version should come either from some backend API or third party service such as Firebase Remote Config
(good experience). If the app is outdated we should show to the user screen/modal/alert that would suggest them to update (redirect to store listing).
How such a modal could look like is included in the template.
useNetInfo()
provides information about current user's network connectivity. In case of isConnected
returns false, we can provide a helpful hint (<OfflineMessage/>
or full screen) to the user that they are disconnected from the internet so that they don't expect full app functionality. Also provide button to either fetch()
the latest connection info or reload
the app should they get stuck.
True is the listener is not fully reliable and I see on my Pixel that I am not connected when changing from background to foreground even though I am
React Native allows as default to scale the font significantly which will break the UI of the app. You should expect users to have font scaling up if your target group includes older generation. To allow some level of accessibility but prevent users from breaking the app UI, default scaling of maximum +25% is applied. You can increase it but be sure to control important parts of the application individually to keep UX in place.
To replicate Figma design consistently on majority of mobile screen sizes, we should apply size scaling to UI elements relative to actual device window width/height. This technique is not perfect and implements a subjective scaling factor, but prevents well having too small elements on larger screens. Inspiration: article + library
-
Styling
-
Restyle, Unistyles, Nativewind
Restyle follows a defined theme with strict type safety resulting in consistent and quickly built UI. It is very helpful when a designer defines majority of text variants which can be plugged into the theme and reused super easily. It has also responsive utilities that can make potential transition to a tablet app easier. Unistyles takes a similar approach but is newer and doesn't require special components. Nativewind is a newer library that is based on Tailwind CSS.
-
-
Notifications
-
React Native Firebase Cloud Messaging + Notifee
Both managed by Invertase with latest notification features. Notifee is needed to change Foreground notifications to local ones. Expo-notifications, alternative to both, is also an option but only with native tokens, because using ExpoPushTokens is a strong lock-in, not easily reverted.
-
-
Forms
-
RHF offers many more utilities (and less bugs) than Formik, e.g. to name one, with
setFocus(fieldName)
one does not have to setup own refs for inputs. Zod for validation is typescript first and type inference is very reliable and useful.
-
-
Bottom Sheets and Modals
-
Reliable all-in-one solution with good Keyboard Handling options
-
-
Swiping content
-
From Callstack, actively maintained and already supporting the new Architecture, uses native components.
-
-
In-App Purchases
-
Ready-to-go solution from RevenueCat, used on Arnold and Showdown projects.
-
We highly recommend using EAS to manage environments.
We map our environment names to these Expo environment names.
"dev": "development",
"staging": "preview",
"production": "production"
Inside Github Actions we use an action to map the environments automatically. To add new environment variable:
- Either add it through the Expo dashboard
- Or through eas cli:
eas env:create <EXPO_ENVIRONMENT> --name <name> --value <value>
⚠️ All variables must use theEXPO_PUBLIC
prefix! e.g.,EXPO_PUBLIC_API_URL
You can also pull variables from EAS to your local .env
file:
eas env:pull <EXPO_ENVIRONMENT> --path <path-to-env-file>