· Simone Saleri · Sviluppo Mobile  · 8 min di lettura

Condividere codice tra React e React Native - Monorepo e React Strict DOM

Tecniche pratiche per massimizzare il riuso del codice tra applicazioni web e native, dall'architettura a monorepo al rivoluzionario approccio di React Strict DOM.

Tecniche pratiche per massimizzare il riuso del codice tra applicazioni web e native, dall'architettura a monorepo al rivoluzionario approccio di React Strict DOM.

Costruire applicazioni sia per il web sia per il mobile ha sempre significato mantenere due codebase separati, con logica duplicata, esperienze utente incoerenti e il doppio del lavoro di manutenzione. Ma non deve per forza essere così. In questo articolo vediamo tecniche collaudate per condividere codice tra React e React Native, dalle strutture a monorepo ormai consolidate fino a React Strict DOM, che cambia le regole del gioco.

Il costo della frammentazione

La prima decisione che uno sviluppatore prende, “web o nativo?”, spesso lo vincola a un insieme specifico di API e paradigmi. Come fa notare Nicolas Gallagher:

Questa biforcazione porta a inefficienze ingegneristiche nascoste e alla necessità di compromessi difficili. Fare due volte un lavoro di interfaccia simile va di pari passo con un time-to-market più lento e una maggiore incoerenza del prodotto.

La frammentazione non incide solo sulla velocità di sviluppo, ma anche sulla programmazione assistita dall’IA. Quando un LLM deve imparare diversi modi di ottenere risultati simili con React, il suo output diventa più lento, meno affidabile e più costoso.

Strategia 1: l’approccio a monorepo

Un monorepo è la base di qualsiasi strategia seria di condivisione del codice. Tenendo le applicazioni web e native in un unico repository, crei l’infrastruttura necessaria per i pacchetti condivisi.

Struttura consigliata

my-app/
├── apps/
│   ├── web/                 # Applicazione web React
│   │   ├── package.json
│   │   └── src/
│   ├── mobile/              # Applicazione React Native
│   │   ├── package.json
│   │   └── src/
│   └── desktop/             # App Electron o Tauri (opzionale)
│       └── ...
├── packages/
│   ├── core/                # Logica di business, stato, utility
│   │   ├── package.json
│   │   └── src/
│   │       ├── hooks/
│   │       ├── services/
│   │       ├── stores/
│   │       └── utils/
│   ├── ui/                  # Componenti UI condivisi
│   │   ├── package.json
│   │   └── src/
│   └── config/              # Configurazione condivisa
│       └── ...
├── package.json
└── turbo.json               # oppure nx.json

Cosa si può condividere

Quasi tutto, tranne l’interfaccia specifica della piattaforma:

  1. Logica di business - chiamate API, trasformazioni dei dati, validazione
  2. Gestione dello stato - store Zustand, Redux o TanStack Query
  3. Hook personalizzati - useAuth, useApi, useDebounce
  4. Tipi TypeScript - interfacce e definizioni di tipo condivise
  5. Utility - formattazione delle date, manipolazione delle stringhe, calcoli
  6. Costanti - endpoint delle API, feature flag, configurazione

Esempio: livello API condiviso

// packages/core/src/services/userService.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

export async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error('Failed to fetch user');
  return response.json();
}

export async function updateUser(id: string, data: Partial<User>): Promise<User> {
  const response = await fetch(`/api/users/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  if (!response.ok) throw new Error('Failed to update user');
  return response.json();
}

Questo servizio funziona in modo identico in React e in React Native: nessun codice specifico per piattaforma.

Esempio: hook personalizzato condiviso

// packages/core/src/hooks/useUser.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchUser, updateUser, User } from '../services/userService';

export function useUser(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
}

export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: Partial<User> }) => updateUser(id, data),
    onSuccess: (data, { id }) => {
      queryClient.setQueryData(['user', id], data);
    },
  });
}

Sia l’app web sia quella mobile importano e usano questo hook esattamente allo stesso modo.

Strategia 2: file specifici per piattaforma

Per i componenti che richiedono implementazioni diverse su web e nativo, usa il pattern delle estensioni .native.js / .web.js:

packages/ui/src/
├── Button/
│   ├── Button.tsx          # Logica e tipi condivisi
│   ├── Button.web.tsx      # Rendering per il web
│   ├── Button.native.tsx   # Rendering nativo
│   └── index.ts

Il bundler risolve automaticamente il file corretto in base alla piattaforma di destinazione.

// Button.tsx - Props e logica condivise
export interface ButtonProps {
  label: string;
  onPress: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}

// Button.web.tsx
import { ButtonProps } from './Button';

export function Button({ label, onPress, variant = 'primary', disabled }: ButtonProps) {
  return (
    <button
      onClick={onPress}
      disabled={disabled}
      className={`btn btn-${variant}`}
    >
      {label}
    </button>
  );
}

// Button.native.tsx
import { ButtonProps } from './Button';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';

export function Button({ label, onPress, variant = 'primary', disabled }: ButtonProps) {
  return (
    <TouchableOpacity
      onPress={onPress}
      disabled={disabled}
      style={[styles.button, styles[variant]]}
    >
      <Text style={styles.label}>{label}</Text>
    </TouchableOpacity>
  );
}

Strategia 3: React Strict DOM, il futuro della condivisione del codice

Monorepo e file specifici per piattaforma funzionano bene, ma richiedono comunque di mantenere due implementazioni separate dell’interfaccia. React Strict DOM rappresenta un cambio di paradigma: scrivi una volta sola usando le API del web, esegui ovunque con qualità nativa.

Cos’è React Strict DOM?

React Strict DOM è una libreria che standardizza lo sviluppo React multipiattaforma sulle API del web. Fornisce un livello di compatibilità rigoroso tra React DOM e React Native:

  • Sul web, i componenti vengono renderizzati come elementi HTML standard e CSS statico
  • Sulle piattaforme native, gli stessi componenti diventano viste native

L’intuizione chiave è che le API del web sono l’interfaccia di programmazione più conosciuta al mondo. Usandole come standard comune, React Native diventa accessibile agli sviluppatori web senza dover imparare un nuovo paradigma.

Come funziona

React Strict DOM esporta due moduli principali: html e css.

import { css, html } from 'react-strict-dom';

const styles = css.create({
  container: {
    display: 'flex',
    padding: '1rem',
    backgroundColor: '#f5f5f5',
  },
  heading: {
    fontSize: '1.5rem',
    fontWeight: 'bold',
    color: '#333',
  },
  text: {
    fontSize: '1rem',
    lineHeight: 1.6,
  },
});

function WelcomeCard({ title, description }) {
  return (
    <html.div style={styles.container}>
      <html.h2 style={styles.heading}>{title}</html.h2>
      <html.p style={styles.text}>{description}</html.p>
    </html.div>
  );
}

Questo codice gira in modo identico su web e nativo. Nessuna logica condizionale, nessun controllo sulla piattaforma: React Strict DOM si occupa della traduzione automaticamente.

Perché è importante

  1. Sblocca l’ecosistema React esistente - milioni di componenti React costruiti per il web diventano candidati all’uso multipiattaforma
  2. Incontra gli sviluppatori dove sono già - quasi tutti gli sviluppatori React conoscono HTML e CSS
  3. Rende la migrazione praticabile - i codemod di Babel possono trasformare i codebase esistenti perché usino React Strict DOM
  4. Protegge l’investimento nel tempo - gli standard web si sono dimostrati straordinariamente stabili per decenni

Validazione in produzione

Meta usa React Strict DOM in produzione dal 2023. Le app VR di Facebook e Instagram, presentate al Meta Connect 2024, dimostrano l’approccio su larga scala:

Oltre il 60% dei file usati dall’app VR di Facebook è condiviso direttamente con facebook.com, comprese alcune delle funzionalità più sofisticate del sito web, come il feed delle notizie, il sistema di commenti, la logica di rendering dei contenuti e il router.

Le app VR sono indistinguibili, per aspetto e sensazione, da app costruite in modo nativo da zero: prova che condividere codice non significa rinunciare alla qualità nativa.

Mettere tutto insieme

L’architettura ideale combina tutte e tre le strategie:

  1. Struttura a monorepo per logica di business, hook e utility condivisi
  2. File specifici per piattaforma per i casi in cui servono implementazioni distinte
  3. React Strict DOM per i componenti UI che possono essere davvero universali
my-app/
├── apps/
│   ├── web/
│   └── mobile/
├── packages/
│   ├── core/                # Logica di business condivisa al 100%
│   │   ├── hooks/
│   │   ├── services/
│   │   └── stores/
│   ├── ui/                  # Componenti React Strict DOM
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   └── Form.tsx
│   └── platform/            # Specifico per piattaforma, quando serve
│       ├── Camera/
│       │   ├── Camera.web.tsx
│       │   └── Camera.native.tsx
│       └── ...

Da dove iniziare

  1. Configura il monorepo con Turborepo, Nx oppure i workspace di Yarn/npm
  2. Estrai la logica condivisa in una directory packages/core
  3. Valuta React Strict DOM per i nuovi componenti UI
  4. Migra in modo incrementale - non devi riscrivere tutto in una volta

L’obiettivo non è condividere il 100% del codice: è massimizzare il riuso dove ha senso, mantenendo la flessibilità di ottimizzare per ogni piattaforma quando serve.

Conclusione

La frammentazione tra React e React Native non è più un costo inevitabile dello sviluppo cross-platform. Con un’architettura a monorepo fatta bene e strumenti emergenti come React Strict DOM, ci stiamo avvicinando a un futuro in cui “scrivi una volta, esegui ovunque” è davvero raggiungibile senza sacrificare la qualità.

Come conclude Nicolas Gallagher: “Quando vincoliamo il modo in cui costruiamo, possiamo creare compromessi che lavorano a nostro favore. Rinunciare a un po’ di flessibilità per aumentare la prevedibilità è ciò che ci permette di comporre sistemi più grandi con più fiducia.”

Inizia in piccolo, condividi ciò che ha senso e amplia gradualmente il codebase condiviso man mano che il team prende confidenza con l’approccio.


Per approfondire:

Articoli correlati

Devi ancora scegliere tra i due prima di arrivare fin qui? React Native o Flutter è la decisione che viene prima. Vale la pena sapere dove ho smesso di condividere: le mie app per Apple Watch sono in Swift nativo, perché i vincoli della piattaforma non sono di quelli che un livello condiviso assorbe bene. Il lavoro su monorepo e moduli nativi è una buona parte di ciò per cui i clienti mi contattano.

Simone Saleri

Simone Saleri — sviluppatore iOS e watchOS. 17 app sull’App Store, quasi tutte app standalone per Apple Watch. Laurea magistrale in Informatica, Università di Milano-Bicocca. Contattami.

Torna al blog

Articoli correlati

Tutti gli articoli »