Monorepo React Native nel 2026: pnpm, Turborepo ed Expo per Team Platform

Configura un monorepo React Native con pnpm 10, Turborepo 2.5 ed Expo SDK 54. Setup Metro, EAS Build 16 e pacchetti condivisi type-safe, con la struttura che uso in produzione su un'app fintech con quattro team paralleli.

Monorepo React Native 2026: pnpm + Turborepo

Aggiornato: 25 luglio 2026

Un monorepo React Native ospita più applicazioni mobile e i loro pacchetti condivisi in un unico repository Git, gestito con pnpm workspaces e orchestrato da Turborepo per build incrementali e cache remota. Nel 2026 questa combinazione è lo standard de facto per i team platform che devono spedire due o tre app Expo, una web app Next.js e cinque o sei librerie interne senza duplicare codice o pipeline CI. In questa guida ti mostro la struttura che uso in produzione su un'app fintech con quattro team paralleli, con Metro, EAS Build e cache remota configurati end-to-end.

  • pnpm 10 con node-linker=hoisted è la scelta più solida per React Native perché Metro non risolve i symlink pnpm di default.
  • Turborepo 2.5 gestisce pipeline, dipendenze tra task e cache remota, abbattendo i tempi di CI del 70–90% su repo con 5+ pacchetti.
  • Metro va configurato con watchFolders sulla root del monorepo e nodeModulesPaths per risolvere le dipendenze hoisted correttamente.
  • EAS Build 16+ supporta i monorepo nativamente tramite il campo build.production.node e packageManager in package.json.
  • I pacchetti condivisi devono esportare TypeScript sorgente (non compilato) tramite condition exports per abilitare hot reload istantaneo tra app.
  • Regola non negoziabile: tutte le app condividono la stessa versione di react, react-native, expo. Usa catalog: di pnpm per bloccarle in un solo posto.

Perché un monorepo per React Native nel 2026

Il momento in cui un team platform inizia a copiare-incollare componenti fra app-clienti e app-operatori è il momento in cui il monorepo diventa inevitabile. In fintech, dove lavoro, gestiamo un'app consumer, una per gli operatori di back-office e una companion per smartwatch: tre binari, tre store, ma una sola libreria di componenti, un solo layer di autenticazione, un solo SDK che parla con le nostre API GraphQL. Senza monorepo, il costo marginale di un cambio di design system sarebbe tre PR in tre repo, tre release, tre finestre di regressione. Non è sostenibile oltre le due sprint.

Nel 2026 il rapporto rischio/beneficio è cambiato ulteriormente in favore del monorepo grazie a tre fattori: Turborepo 2.5 ha stabilizzato la sua cache remota basata su S3-compatible storage; pnpm 10 ha introdotto la sintassi catalog: per pinnare versioni in un unico file; ed Expo SDK 54 ha semplificato il supporto monorepo in EAS Build eliminando la necessità di eas-cli-local-build-plugin e simili workaround. Se avessi iniziato oggi un nuovo prodotto mobile, non aprirei nemmeno il template create-expo-app stand-alone: partirei direttamente da un template monorepo. Se vuoi il quadro completo dell'infrastruttura di runtime che gira sotto, dai un'occhiata alla mia guida alla Nuova Architettura React Native: il monorepo è il livello sopra, ma le due decisioni si intrecciano parecchio.

pnpm workspaces vs Yarn Berry vs npm: quale gestore scegliere

La domanda che ricevo più spesso in code review è: "Perché pnpm e non Yarn Berry?". La risposta breve: perché Metro, il bundler di React Native, non risolve correttamente i symlink profondi che Yarn PnP (Plug'n'Play) genera, e la modalità nodeLinker: node-modules di Yarn Berry perde molti dei vantaggi che si andrebbero a cercare. pnpm 10 con node-linker=hoisted ti dà il meglio dei due mondi: struttura piatta compatibile con Metro, e installazioni fino a 3× più veloci grazie allo store globale content-addressable.

Caratteristicapnpm 10Yarn Berry 4npm 11
Compatibilità MetroOttima (con node-linker=hoisted)Problematica (PnP incompatibile)Buona
Velocità installazione~2–3× più veloce di npmSimile a pnpmBaseline
Uso discoMinimo (store globale)MinimoElevato (duplicazione)
Catalog versioningSì (nativo dalla v9.5)Sì (constraints)No
EAS Build supportoNativo dalla SDK 51Richiede workaroundNativo
Curva di apprendimentoMediaAltaBassa

Nella mia esperienza, il fattore decisivo per un team platform non è la velocità di install, ma la correttezza delle dipendenze fantasma: pnpm impedisce a un pacchetto di importare qualcosa che non ha dichiarato in package.json, cosa che Yarn e npm classici permettono. Questo elimina un'intera classe di bug che compaiono solo in CI dopo un refactor (ho perso un venerdì pomeriggio esattamente su questo scenario, prima di migrare a pnpm). Consulta la documentazione ufficiale pnpm workspaces per approfondire la modalità hoisted.

Struttura di cartelle consigliata per un monorepo React Native + Expo

Dopo tre monorepo di produzione, sono arrivato a una struttura in due livelli: apps/ per i binari eseguibili e packages/ per il codice riutilizzabile. Non aggiungere un terzo livello: se ti serve, probabilmente stai facendo overengineering. Ecco lo scheletro che replico su ogni nuovo repo:

my-fintech/
├── apps/
│   ├── mobile-customer/          # app Expo consumer
│   ├── mobile-operator/          # app Expo back-office
│   └── web-dashboard/            # Next.js 15 admin
├── packages/
│   ├── ui/                       # componenti condivisi (Tamagui/Unistyles)
│   ├── api-client/               # SDK generato da GraphQL Codegen
│   ├── config/                   # eslint, tsconfig, prettier base
│   ├── auth/                     # logica OAuth condivisa
│   └── analytics/                # wrapper eventi tipizzati
├── tooling/
│   └── scripts/                  # script Node per build/deploy
├── pnpm-workspace.yaml
├── turbo.json
├── package.json
└── .npmrc

Il file pnpm-workspace.yaml dichiara gli scope e (cosa fondamentale nel 2026) il catalog con le versioni pinnate:

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'tooling/*'

catalog:
  react: 19.1.0
  react-native: 0.81.5
  expo: ~54.0.0
  typescript: 5.7.3
  '@tanstack/react-query': 5.62.0

Nel package.json di ogni app o pacchetto usi poi "react": "catalog:" invece della versione esplicita. Questo elimina il problema numero uno dei monorepo React Native: due app che stanno accidentalmente su versioni diverse di react-native, con crash a runtime imprevedibili.

Il file .npmrc è breve ma non negoziabile:

# .npmrc
node-linker=hoisted
public-hoist-pattern[]=*
strict-peer-dependencies=false
auto-install-peers=true

Configurare Metro per un monorepo (watchFolders e nodeModulesPaths)

Metro di default cerca dipendenze e file solo dentro la cartella dell'app. In un monorepo devi dirgli esplicitamente "guarda anche fuori", altrimenti gli import da @my-fintech/ui falliranno con l'errore Unable to resolve module. Ecco la configurazione minima che uso in ogni apps/mobile-customer/metro.config.js:

// apps/mobile-customer/metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');

const config = getDefaultConfig(projectRoot);

// 1. Osserva tutta la root del monorepo per hot reload cross-package
config.watchFolders = [workspaceRoot];

// 2. Cerca node_modules in entrambe le posizioni (locale + root)
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(workspaceRoot, 'node_modules'),
];

// 3. Disabilita hierarchical lookup, evita risoluzioni ambigue
config.resolver.disableHierarchicalLookup = true;

module.exports = config;

Su Xcode e Android Studio, l'unica accortezza aggiuntiva è che la cartella ios/ e android/ rimangono dentro apps/mobile-customer/. Non spostarle nella root, CocoaPods e Gradle si aspettano i file nativi lì. Se hai bisogno di un modulo nativo custom condiviso tra app, mettilo in packages/native-foo/ con il pattern Expo Modules API: ne parlo in dettaglio nella guida a Expo Modules API.

Turborepo 2.5: pipeline, cache remota e task orchestration

Turborepo è il motore che trasforma un monorepo in una macchina di build efficiente. La sua funzione principale non è "eseguire task", ma saltarli quando l'output è già in cache. In un CI tipico, dopo aver toccato una riga in packages/ui, Turborepo ri-esegue il typecheck e il test solo dei pacchetti che dipendono da ui, riprendendo il resto dalla cache. Sui nostri repo, il tempo medio di CI è passato da 14 a 2 minuti dopo l'attivazione della cache remota (numero misurato, non stimato).

La configurazione vive in turbo.json:

// turbo.json
{
  "$schema": "https://turborepo.com/schema.json",
  "ui": "tui",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"],
      "inputs": ["src/**", "package.json", "tsconfig.json"]
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**", "test/**", "jest.config.*"]
    },
    "start": {
      "cache": false,
      "persistent": true
    }
  }
}

Le due chiavi da capire davvero sono dependsOn e inputs. dependsOn: ["^build"] significa "prima esegui il build di tutte le dipendenze topologicamente precedenti". inputs definisce l'hash della cache: se cambi solo README.md, l'hash resta uguale e il task salta. Ometti inputs e Turborepo hasherà l'intero pacchetto, con cache-miss inutili garantiti.

Per la cache remota, il modo più semplice nel 2026 è collegare Vercel Remote Cache gratuito (npx turbo login && npx turbo link), oppure self-hostare con turborepo-remote-cache su un bucket S3 o R2 se il vincolo di compliance lo richiede. In fintech è la nostra scelta, per motivi ovvi di residenza del dato.

Pacchetti condivisi: UI, config e types type-safe

Un errore comune è pre-compilare i pacchetti condivisi in dist/ con tsc. In un monorepo React Native, questo rompe il fast refresh: modifichi packages/ui/Button.tsx e devi ri-buildare prima di vedere il cambiamento nell'app. La soluzione elegante è esportare direttamente il TypeScript sorgente attraverso i condition exports, lasciando che Metro (che integra Babel) faccia la trasformazione al volo.

// packages/ui/package.json
{
  "name": "@my-fintech/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "react-native": "./src/index.ts",
      "default": "./src/index.ts"
    }
  },
  "peerDependencies": {
    "react": "catalog:",
    "react-native": "catalog:"
  },
  "devDependencies": {
    "typescript": "catalog:"
  }
}

Per la type-safety cross-package, usa TypeScript project references. Ogni tsconfig.json dichiara i pacchetti da cui dipende, e un tsconfig.json alla root li orchestra:

// tsconfig.json (root)
{
  "files": [],
  "references": [
    { "path": "./packages/ui" },
    { "path": "./packages/api-client" },
    { "path": "./packages/auth" },
    { "path": "./apps/mobile-customer" }
  ]
}

// packages/ui/tsconfig.json
{
  "extends": "@my-fintech/config/tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

Come funziona EAS Build in un monorepo

EAS Build 16 (rilasciato a febbraio 2026) supporta i monorepo senza plugin aggiuntivi, ma devi dichiararlo esplicitamente. In apps/mobile-customer/eas.json configura:

// apps/mobile-customer/eas.json
{
  "cli": {
    "version": ">= 16.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "production": {
      "node": "22.11.0",
      "pnpm": "10.4.0",
      "env": {
        "EAS_BUILD_ENABLE_CACHE": "1"
      },
      "cache": {
        "key": "monorepo-v1",
        "paths": ["../../node_modules/.pnpm-store"]
      }
    }
  }
}

Sul lato package.json della root, aggiungi packageManager: EAS Build lo legge per decidere quale binario installare.

// package.json (root)
{
  "name": "my-fintech-monorepo",
  "packageManager": "[email protected]",
  "engines": {
    "node": ">=22"
  }
}

EAS di default esegue il build dalla directory dell'app, ma nel nostro caso deve prima installare tutte le dipendenze dal workspace. Il flag appDirectory nel profilo di build gestisce questo automaticamente se lanci eas build da apps/mobile-customer. Se hai già letto la mia guida a EAS Build e Submit, i profili di release e i credential Apple/Google restano identici, cambia solo l'installazione dipendenze. La documentazione ufficiale Expo monorepos copre i casi limite per Yarn e npm se preferisci quelli.

CI/CD con GitHub Actions e cache di Turborepo

Il pattern che raccomando è un workflow che sfrutti sia la cache di Turborepo sia quella di GitHub Actions per pnpm-store. Onestamente, è la parte del setup che ripaga di più nel tempo: dopo il primo giorno ci si dimentica di averla, ma senza si tornerebbe ad aspettare ogni push. Ecco un esempio funzionante e testato in produzione:

# .github/workflows/ci.yml
name: CI
on:
  pull_request:
    branches: [main]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # necessario per turbo --filter=[HEAD^1]

      - uses: pnpm/action-setup@v4
        with:
          version: 10.4.0

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Restore Turborepo cache
        uses: actions/cache@v4
        with:
          path: .turbo
          key: turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }}
          restore-keys: |
            turbo-${{ github.job }}-${{ github.ref_name }}-
            turbo-${{ github.job }}-

      - name: Lint, typecheck and test
        run: pnpm turbo lint typecheck test --filter=[HEAD^1]
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Il flag --filter=[HEAD^1] è il moltiplicatore di velocità reale: dice a Turborepo di eseguire solo i task dei pacchetti effettivamente cambiati rispetto al commit precedente. Su un monorepo con 12 pacchetti, un cambio localizzato a packages/analytics riduce la matrice CI da 12 a 2 pacchetti. Combinato con la cache remota, il PR medio passa il verde in 90 secondi.

Errori comuni e come evitarli

Ho debuggato abbastanza monorepo React Native da riconoscere i pattern che si ripetono. Ecco i cinque errori più costosi:

  1. Versioni di React duplicate. Errore Invalid hook call a runtime perché due app hanno pinnato react su versioni minor diverse. Fix: usa catalog: di pnpm ovunque e valida con pnpm why react.
  2. Metro che non trova un pacchetto. Manca watchFolders o il pacchetto non è dichiarato in dependencies dell'app che lo importa. Non basta averlo nel workspace.
  3. EAS Build che scarica dipendenze dal registro pubblico per pacchetti privati interni. Fix: assicurati che private: true sia in ogni package.json interno e che "@my-fintech/ui": "workspace:*" sia scritto esattamente così nelle app.
  4. Cache Turborepo che non hitta mai in CI. Nove volte su dieci è perché outputs nella task non include tutti i file generati (es. .next/cache/** escluso di default). Attiva TURBO_LOG_VERBOSITY=2 per vedere l'hash e capire cosa lo invalida.
  5. Symlink pnpm che rompono Hermes. Se vedi errori di risoluzione a runtime su iOS release build, verifica di aver messo node-linker=hoisted in .npmrc prima di aver eseguito la prima installazione. Se lo cambi dopo, esegui pnpm install --force e ripulisci ios/Pods. Io questo bug l'ho beccato la prima volta ad agosto 2024 su una build TestFlight, e non lo dimentico più.

Domande frequenti

Posso usare pnpm con React Native e Expo nel 2026?

Sì, pnpm 10 è pienamente supportato da React Native 0.81+ ed Expo SDK 54, a patto di impostare node-linker=hoisted in .npmrc. EAS Build rileva automaticamente pnpm leggendo il campo packageManager nel package.json della root.

Turborepo è meglio di Nx per React Native?

Turborepo è più semplice da configurare e più veloce per l'uso specifico "orchestrare pnpm scripts con cache remota". Nx offre generatori, plugin dedicati React Native e un grafo di dipendenze visuale, ma introduce complessità che raramente ripaga in team sotto le 20 persone. Per un team platform tipico, Turborepo vince.

Come si condividono componenti UI tra due app Expo?

Crea un pacchetto packages/ui con main che punta al TypeScript sorgente (./src/index.ts), aggiungilo alle dependencies di ogni app come "@scope/ui": "workspace:*", e configura Metro con watchFolders sulla root del monorepo. Fast refresh funzionerà cross-package senza compilazione intermedia.

Serve un monorepo se ho una sola app React Native?

No, se prevedi di rimanere con un solo binario. Il monorepo diventa vantaggioso quando aggiungi una seconda app, una web companion, un backend condiviso, o quando i pacchetti interni superano tre unità. Prima di quel punto, il costo di configurazione supera il beneficio.

Come funziona la cache remota di Turborepo in un ambiente enterprise?

Puoi collegare Vercel Remote Cache (gratuito per team fino a 10 utenti) oppure self-hostare con il progetto open source turborepo-remote-cache su S3, R2 o MinIO. Per compliance in fintech o healthcare, l'opzione self-hosted è quasi sempre obbligatoria.

Yelena Petrov
Sull'Autore Yelena Petrov

React Native architect at a fintech. Builds platform teams, type-safe bridges, and runs the upgrade playbook so others don't have to.