Expo Router, monorepos and the wider ecosystem
Use file-based routing, share code across platforms and packages, structure a growing repo, and choose wisely when picking dependencies.
File-based routing with Expo Router
app/
_layout.tsx root stack, providers, theme
(tabs)/
_layout.tsx bottom tabs
index.tsx / (home tab)
search.tsx /search
(settings)/
_layout.tsx nested stack inside a tab
index.tsx /settings
notifications.tsx /settings/notifications
task/
[id].tsx /task/123
+not-found.tsx unmatched routes and bad deep links// app/task/[id].tsx
import { useLocalSearchParams, useRouter } from 'expo-router';
export default function TaskDetail() {
const { id, from } = useLocalSearchParams<{ id: string; from?: string }>();
const router = useRouter();
return (
<View>
<Text>Task {id}</Text>
<Button title="Back" onPress={() => (from ? router.replace(from) : router.back())} />
</View>
);
}- Route file names are the URL, so a deep link to
/task/123works with no extra configuration on web and mobile. - A
_layoutfile defines the navigator for everything beneath it, and parentheses group routes without adding a path segment. useLocalSearchParamsgives typed access to path and query parameters.- Keep screens in
app/and reusable components outside it, or a file becomes a route by accident.
Sharing code across packages
{
"name": "workspace",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"mobile": "yarn workspace @acme/mobile start",
"web": "yarn workspace @acme/web dev",
"typecheck": "yarn workspaces foreach -pt run typecheck"
}
}
// packages/domain/package.json
// { "name": "@acme/domain", "main": "src/index.ts", "types": "src/index.ts" }| Package | Contents | Consumers |
|---|---|---|
@acme/domain | Entities, rules, pure functions | Mobile, web, server |
@acme/api | Client, schemas, generated types | Mobile and web |
@acme/ui | Platform-aware components | Mostly web, selectively mobile |
@acme/mobile | The app target | n/a |
Metro needs to resolve symlinked workspace packages and, in the New Architecture, which native module is registered. Configure watchFolders and nodeModulesPaths explicitly rather than fighting resolution errors later.
Choosing dependencies and growing a team
// scripts/check-deps.mjs: fail CI on a dependency that is not approved
import { readFileSync } from 'node:fs';
const approved = new Set(JSON.parse(readFileSync('approved-deps.json', 'utf8')));
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
const used = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
const unapproved = used.filter((name) => !approved.has(name));
if (unapproved.length) {
console.error('Unapproved dependencies:', unapproved.join(', '));
process.exit(1);
}💡
The ecosystem moves fast and many popular libraries are maintained by one person. Before adopting one, check the release cadence, whether it ships types, and whether it has a New Architecture path — replacing a core dependency later costs far more than choosing carefully now.
FAQ
Expo Router or React Navigation directly?
Expo Router is built on React Navigation and adds file-based routes, typed links and first-class web support. Choose it for a new app; choose React Navigation directly only if you need direct control over the navigator tree.
Should a small team start with a monorepo?
Only once there is a second consumer of the shared code. A single app with clear internal boundaries is easier to work in, and extracting a package later is a mechanical refactor.
Related
Building and releasing with EAS and Fastlane State management: Context, Redux Toolkit and Zustand
Last refreshed 2026-09-18.