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/123 works with no extra configuration on web and mobile.
  • A _layout file defines the navigator for everything beneath it, and parentheses group routes without adding a path segment.
  • useLocalSearchParams gives 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" }
PackageContentsConsumers
@acme/domainEntities, rules, pure functionsMobile, web, server
@acme/apiClient, schemas, generated typesMobile and web
@acme/uiPlatform-aware componentsMostly web, selectively mobile
@acme/mobileThe app targetn/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.

Building and releasing with EAS and Fastlane State management: Context, Redux Toolkit and Zustand

Last refreshed 2026-09-18.