- the aim of this project is to implement a sane DX and a CI/CD workflow using Github Actions and Expo EAS
- we have 3 builds/apps: Development, Staging and Preview
- developers can build features using a browser
- the UI team can review UI components using Chromatic/Storybook
- the whole team can view updates for each of these 3 apps along the development process
- Yarn
- TypeScript
- react-native
- Expo
- NextJS
- Solito
- Tamagui
- Storybook
- Jest, testing-library
- Turbo
This is a monorepo for an Expo + NextJS app ⚡️
While our main objective is to develop a mobile app, now thanks to Solito and Tamagui we can target the web also. Just Tamagui alone brings great value due to its performance and theme-ui styles, while Solito provides cross-platform navigation.
react-native development is a bit tedious compared to web development, requiring testing on different platforms (iOS, Android), different OS versions, and different screen sizes. Even the processes of app building, submission and updating can get complicated without Expo. This is why this repo aims to easen the effort when it comes to native development, while implementing quality checks and CI/CD flows.
In this regard, the main reason for choosing to have web compatibility is the Developer Experience. From my personal experience building react-native apps, working with just the Simulator to build UIs can get frustrating, like when debugging with an attached Chrome Dev Tools instance (can't use Inspect Element on DIVs to tinker with styles, for example) or using the in-app injected Debugger (sort of a dumbed down Dev Tools, not really productive). But with this stack, we can build our mobile app using the browser, as you would do with a normal web app. Sure, one might implement mobile-specific features which won't work on web, so in this case they have to be properly handled, but in our case we're more interested to use a browser for UI development especially. There are some unsupported react-native APIs in react-native-web, which is used by NextJS and Storybook, but as long as we don't hit these limitations, we can continue using web.
Another good reason is Tamagui itself. It has the best performance compared to any other react-native UI libs.
Another reason for organizing everything in a monorepo is because we're targeting 2 different platforms (web & mobile), each having its own config. Overall, we're keeping each package clean of unwanted dependencies and with their own specific structure and configuration that are easy to follow.
This monorepo is the result of:
- initial scaffold using
npm create tamagui
(see the docs) - getting some inspiration from
tamagui-kitchen-sync
for adding Storybook to it (link) - adding
turbo
as a build and task runner cache system (link) - using
Yarn Workspaces
with plugins (docs) - tweaking
tsconfig.json
andpackage.json
for each workspace package to glue them together nicely with TypeScript Project References and Path Aliases (docs). One useful trick was to define path aliases with the same name as the ones defied by the workspace packages; this way, the code editor will jump on click to source, instead of node_modules - setting up
build
,format
,lint
,typecheck
for each package - setting up
husky
withpre-commit
,commitlint
andlint-staged
hooks - setting up
semantic-release
forapps/expo
- getting some inspiration for Github Actions cache from
nextjs-monorepo-example
(link) - setting up a productive development workflow involving Github Actions + Chromatic/Storybook + Expo EAS (chromatic docs, EAS docs)
- overall nitting and bolting everything together, occasionally with some help from the fantastic Discord communities of Tamagui, Turbo and Expo
An easier alternative for setting up a monorepo would've been NX (link), which I tried initially and didn't work out because:
- you can leverage the full power of NX only in its
integrated
mode and not inpackage
mode - integrated means you have only one root
package.json
+ TS path aliases, and each app configuration is black-boxed behind NX custom config generators - you lose the flexibility of running custom
package.json
commands for your internal packages - if choosing package mode, you would be better off to ditch NX completely and manually configure Yarn + TypeScript + Turbo as I did
- NX enforces that each custom environment variable has to be prefixed with
NX_
while packages liketamagui
expect exactlyTAMAGUI_TARGET
orAPP_VARIANT
- overall, individual
package.json
s withdotenv
do the trick
apps
expo
next
storybook
tests
packages
shared packages across appsapp
you'll be importing most files fromapp/
features
(don't use ascreens
folder, instead organize by feature). We should organize our code with respect to Domain Driven Design (blog article)provider
(all the providers that wrap the app, and some no-ops for Web.)navigation
Next.js has apages/
folder. React Native doesn't. This folder contains navigation-related code for RN. You may use it for any navigation code, such as custom links.
eslint-config-custom
as a global ESLint config, used in packages as{ extends: ["custom", ...] }
tsconfig
for specific TypeScript configuration for each packageui
for our dumb UI componentsbackend
- not yet in the project, but will host our Supabase PostgreSQL Prisma schemas, Deno edge functions + optionally tRPC. One useful backend feature will be to trigger user notifications. Can get some inspiration from the T3 stack
The dependency graph looks like this:
- create an Expo account and a new Expo team
- create a Turbo team
- create a Chromatic account
- search this project for "REPLACE-ME" and replace stuff accordingly
- go over Prerequisites for development as described below and make sure everything works: running Storybook, running the app in Next, simulators, on your device with Expo
- go over Setup Github Actions as described below
- create your first builds with Expo as described in App variants below
- at this point you can safely start developing as described in Workflow below: Github Actions, Chromatic updates and EAS Updates should work as expected
- see eas.json
- builds are real apps with different identifiers:
dev.com.REPLACE-ME
,staging.com.REPLACE-ME
,preview.com.REPLACE-ME
, see app.config.ts, docs, expo/package.json build commands - for first time builds, use:
yarn build:all:dev
yarn build:all:staging
yarn build:all:preview
- 2 platforms x 3 variants = 6 builds. They can be found here
- all builds should have
1.0.0
version. They should only be rebuilt when the runtime policy ofsdkVersion
kicks in, meaning when we're forced to update if the Expo SDK version changes, see app.json, docs. What changes automatically after each release isbuildNumber
(for iOS) andversionCode
(for Android) - your team has to enable Developer mode to be able to run these apps
- updates to these apps are delivered via EAS Update (docs), instead of always building new app versions on each release (the Expo free tier offers only 20 builds / month and 1000 updates, so we prefer leveraging EAS Updates)
- The Development app is useful only in tandem with
yarn start:devclient
to preview live changes from a developer's machine. Could be used for pair programming. - The Staging app is updated on each opened PR. Useful for code reviews (limitation: 1 PR to be opened at a time)
- The Preview app is updated on each new git release (merges to main) and acts as an exact preview of the Production app (which hasn't been built yet) used for testing before an official release. Team demos use this app.
- set the following secrets
CHROMATIC_PROJECT_TOKEN
- you can get one after you create a Chromatic projectEXPO_TOKEN
- get it from your Expo accountEXTENDED_GITHUB_TOKEN
- this is a Github Personal Access Token, get one from your account; needs write access. Used by semantic-release to push new tagsSLACK_WEBHOOK
- a Slack webhook configured to route messages to a channel, from Github ActionsTURBO_TEAM
- follow Turbo docs how to create a teamTURBO_TOKEN
- see Turbo docs
- create the following environment variables:
- for
staging
environmentAPP_VARIANT
=stagingTAMAGUI_TARGET
=native
- for
preview
environmentAPP_VARIANT
=previewTAMAGUI_TARGET
=native
- for
- install Android Studio Emulator
- install iOS Simulator
- install Flipper (useful for debugging RN apps)
- join the Expo project (invites were sent)
- join the Chromatic project, here
- install the development, staging & preview builds on your personal device, taken from here
- run
npm install -g eas-cli
. For more info check EAS Build - read the docs for Expo, Tamagui, Turbo
- run
yarn
to install dependencies - run
yarn start:expo
to start Expo - run
yarn start:next
to start Next - run
yarn start:sb
to start Storybook
- The developer creates a new branch off
main
- Starts Storybook
yarn start:sb
and Expoyarn start:expo
- mainly uses Chrome + Storybook for UI development in
packages/ui
- can also use Chrome + NextJS for app logic development in
packages/app
- runyarn start:next
- checks his changes inside the iOS Simulator and Android Studio (press "i" / "a" in the console)
- checks the app on his real mobile device using the consle QR Code
- can share his changes live before opening a PR with other devs in 2 ways:
- using Expo Go, by running
yarn start:tunnel
and sharing the QR code with others - using the Development Build Client (has to be installed by others), running
yarn start:devclient
and sharing the QR Code - the difference between these two is that the Development Build Client is more closer to the production app
- using Expo Go, by running
- checks the minified app running
start:ios:prod
andstart:android:prod
. Same asyarn start:expo
, but it's a minified and closer to production build. Sometimes you can catch some 3rd party lib bugs this way.
- Commits his changes
- commit messages must respect the conventional commit format, eg:
fix(login screen): adjusted font size
- on commit, husky runs
npx lint-staged --allow-empty && yarn g:run-all-checks
, where the checks are:turbo run typecheck lint format:check test build --parallel --since=HEAD^1
- Pushes his changes and opens a new PR
- Gihub Actions kick in:
ci-build-test.yml
: runs Typecheck, Lint, Format check, Build, Unit tests for all configured packagesci-monorepo-integrity.yml
: Check for duplicate dependencies and yarn constraintscd-chromatic.yml
: Deploys the Storybook to Chromatic to be reviewed by the team ❗️cd-eas-update-staging.yml
: Updates the Staging app to be reviewed by the team ❗️. Also a QR code is automaticallty generated in the PR comments and links to the Expo Go client.
- requires at least 1 reviewer to accept the PR
- On merging the PR to
main
, the same Github Actions run again, with the following additional actions:
ci-semantic-release.yml
: Runs semantic-release which bumps the version ofapps/expo/package.json
, bumpsbuildNumber
andversionCode
forapps/expo/app.json
, updatesapps/expo/CHANGELOG.md
by aggregating the conventional commit messages, creates a new git tag version and a new git release based on it. See Releases. Please join the#REPLACE-ME-github
Slack channel for updates 📫cd-eas-update-preview.yml
: Triggered by a new git release, updates the Preview app
- we have yet to document the bundle size, but my memory recalls 1.9M for this barebone project
- Tamagui by itself delivers great performance with respect to style updates and animations, leaving others in dust
- Github Actions CI checks take around 10 minutes without Turbo cache, and around 2-3 minutes with Turbo cache
- Manual Expo Builds take around 10 minutes using the Free Trial (we anyway trigger new builds very rarely because we leverage CI EAS hot Updates instead). Putting this in context, building in CI by yourself with fastlane takes around 1H from my experience (yeah very debatable, but still).
- which libs to use for state management and data fetching? RTK handles both well in an integrated fashion. Jotai is another strong candidate, as it has React-Query integration and tRPC integration (we want to use tRPC on the backend)
If you're installing a JavaScript-only dependency that will be used across platforms, install it in packages/app
:
cd packages/app
yarn add date-fns
If you're installing a library with any native code, you must install it in apps/expo
:
cd apps/expo
yarn add react-native-reanimated
- go over each
package.json
to get accustomed with NPM commands specific to each package - the root
package.json
should contain only monorepo specific commands
- this repo serves as a good starting point for development with Expo EAS, and will be improved in terms of workflow and tooling as we progress and discover new stuff that we might need, for example:
- for Storybook, we might swap Webpack in favor of Vite or Turbopack because of the slow build time
- better testing architecture? Right now
apps/tests
is configured to run user interaction tests withtesting-library
(docs). We might want to add Snapshot testing and E2E Detox tests. Or picking between movingjest
to each individual package VS root jest config withprojects: [...]
. Not a priority right now. - Tamagui is being actively developed and sometimes breaking changes occur. We have to keep an eye on it.
- Running Turbo with
--since=HEAD^1
should be refactored to select the PR's first commit hash instead - no concrete UI components have been developed up to this point, and we should develop them along with a custom theme. We don't want to repeat text sizes, colors and spacings everywhere.
- missing from our workflow is the production distribution of the app to stores, and most probably it won't be integrated with Continous Deployment, because we want to control this sensible process manually. But we want to trigger Continous Deployment EAS Updates to the production app on pushing to a
production
branch, but hasn't been configured yet. There are some Deployment patterns and we will pick one at the right time. Right now we're usingeas update --branch staging/preview
in our Github Actions which are linked to thestaging/preview
channels. These Expo branches shouldn't be confused with git branches: a build corresponds to a release channel, and these "branches" are pointed to channels. When using theeas update
command, regardless of the git branch you're on, you can specify--branch
so that your code can be routed to the specific channel.--auto
sets the branch to the current git branch, but we're not using that. - some tweaking is needed around file globs used in Turbo and Github Actions, to prevent running unnecessary commands (eg: it doesn't make sense to trigger a typecheck when only the README.md file was updated)
- we should be really careful when upgrading react, react-native, react-native-web, tamagui. Experience and public Github issues show us that things can break really easily for lots of people at once and often. Manual and automated tests are key.
- before going public, we should scan and update our dependencies with Snyk, update this readme with respect to Release Management and check again all permissions regarding this process
-
if
yarn ios
ornpx pod-install
fails with:Couldn't install Pods. Updating the Pods project and trying again... Command `pod install` failed. └─ Cause: Invalid `Podfile` file: [!] Invalid `RNGestureHandler.podspec` file: undefined method `exists?' for File:Class.
Check this facebook/react-native#35807 (comment)
-
devs are encouraged to update this section any time they run into unexpected problems