Generative UI in React: guida pratica
Come implementare componenti generati dall'AI nelle applicazioni React: pattern concreti e i cinque errori che fanno fallire i prototipi in produzione.
La maggior parte dei prototipi GenUI fallisce su questi cinque pattern
Le demo di Generative UI sembrano magia. Le applicazioni GenUI in produzione si rompono in modo prevedibile in cinque punti: scelta fragile degli strumenti, race condition durante lo streaming, disallineamento dei prop a runtime, assenza di fallback quando il modello non è disponibile, e costo di inferenza fuori controllo. Questa guida tratta i cinque pattern che tengono viva una feature GenUI oltre la demo — registro, separazione, skeleton, error boundary e stato — insieme ai compromessi che ciascuno nasconde, e con raccomandazioni concrete per due tipi di lettore che di solito decidono "si rilascia o no": l'engineering manager che sceglie lo stack e lo sviluppatore indie che lancia un side project con budget limitato.
Perché React per la Generative UI?
Il modello a componenti di React è particolarmente adatto alla Generative UI. I componenti sono componibili, tipizzati e possono essere renderizzati sul server o sul client. Quando un modello AI "genera UI," quello che fa concretamente è selezionare e comporre componenti React con prop specifiche.
Questa guida copre i pattern che funzionano in produzione e gli errori che vedo commettere ai team quando iniziano a sviluppare interfacce generative. Do per scontato che tu abbia un setup Next.js funzionante e che tu abbia letto le basi del Vercel AI SDK — questa è la parte pratica che si costruisce su quella fondazione.
Pattern 1: Il registro degli strumenti
La base di qualsiasi sistema Generative UI manutenibile è un registro esplicito e centralizzato dei componenti che l'AI può usare. Non disperdere le definizioni degli strumenti tra le server action.
// lib/genui-registry.ts
import { z } from 'zod';
import { MetricCard } from '@/components/metric-card';
import { DataTable } from '@/components/data-table';
import { BarChart } from '@/components/bar-chart';
import { AlertBanner } from '@/components/alert-banner';
import { LineChart } from '@/components/line-chart';
export const tools = {
metricCard: {
description: 'Display a single KPI metric with a trend indicator. Use for scalar values like revenue, user count, or conversion rate.',
parameters: z.object({
label: z.string().describe('The metric name, e.g. "Monthly Revenue"'),
value: z.string().describe('The formatted value, e.g. "$12,400"'),
change: z.number().describe('Percentage change vs. previous period'),
period: z.string().describe('The comparison period, e.g. "vs last month"'),
}),
component: MetricCard,
},
dataTable: {
description: 'Display tabular data with sortable columns. Use when showing lists of items with multiple attributes.',
parameters: z.object({
columns: z.array(z.object({
key: z.string(),
label: z.string(),
numeric: z.boolean().optional(),
})),
rows: z.array(z.record(z.string())),
caption: z.string().optional(),
}),
component: DataTable,
},
barChart: {
description: 'Display a bar chart for categorical comparisons. Use when comparing values across discrete categories.',
parameters: z.object({
title: z.string(),
data: z.array(z.object({ label: z.string(), value: z.number() })),
yAxisLabel: z.string().optional(),
}),
component: BarChart,
},
lineChart: {
description: 'Display a line chart for time-series data. Use when showing trends over time.',
parameters: z.object({
title: z.string(),
data: z.array(z.object({ date: z.string(), value: z.number() })),
unit: z.string().optional(),
}),
component: LineChart,
},
alertBanner: {
description: 'Display an important notice, warning, or success message. Use sparingly for genuinely important information.',
parameters: z.object({
type: z.enum(['info', 'warning', 'error', 'success']),
title: z.string(),
message: z.string(),
}),
component: AlertBanner,
},
};
export type ToolName = keyof typeof tools;
Intuizione chiave: Il campo description è ciò che l'AI legge per decidere quale componente usare. Scrivi le descrizioni per l'AI, non per gli esseri umani. Sii specifico su quando ogni componente è appropriato e, soprattutto, quando non lo è.
Nota che lineChart dice "time-series" e barChart dice "categorical." Senza questa distinzione, l'AI fa scelte casuali tra i due. Più precise sono le tue descrizioni, migliore sarà la selezione dei componenti.
Quando il pattern non funziona. Un registro centralizzato presuppone che un solo team ne sia proprietario. Se tre team di prodotto vogliono i propri componenti, il registro diventa un collo di bottiglia di coordinamento — ogni nuovo strumento passa per una PR al team di piattaforma. L'alternativa è un registro federato per superficie di prodotto, al costo di descrizioni duplicate e qualità divergente. Centralizzazione per un singolo prodotto, federazione per una piattaforma che serve molti. Vedi la documentazione ufficiale di streamUI nel Vercel AI SDK per le API di base.
Pattern 2: Separare il registro dallo streaming
Mantieni la definizione del registro separata dalla chiamata a streamUI. Questo ti permette di riutilizzare le definizioni degli strumenti in più server action e di testare il registro in isolamento.
// lib/stream-with-tools.ts
import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { tools } from './genui-registry';
// Convert registry format to streamUI format
function buildStreamTools(toolNames: ToolName[]) {
return Object.fromEntries(
toolNames.map((name: ToolName) => [
name,
{
description: tools[name].description,
parameters: tools[name].parameters,
generate: async function* (params: unknown) {
yield <ToolSkeleton name={name} />;
const Component = tools[name].component;
// Guardia contro null: un'entry del registro potrebbe essere errata
// o ricaricarsi durante un hot reload nel mezzo di una richiesta.
if (!Component) {
return <GenUIFallback error={new Error(`Missing component for tool: ${name}`)} resetErrorBoundary={() => {}} />;
}
return <Component {...(params as Record<string, unknown>)} />;
},
},
])
);
}
// Server action for a data dashboard
export async function generateDashboard(query: string) {
const result = await streamUI({
model: openai('gpt-4o'),
system: 'You are a data analyst assistant. Display information using the appropriate visualization tool.',
prompt: query,
tools: buildStreamTools(['metricCard', 'dataTable', 'barChart', 'lineChart', 'alertBanner']),
});
return result.value;
}
// Server action for a summary view (fewer tools = better focus)
export async function generateSummary(query: string) {
const result = await streamUI({
model: openai('gpt-4o'),
system: 'You are a concise assistant. Show a summary with key metrics only.',
prompt: query,
tools: buildStreamTools(['metricCard', 'alertBanner']),
});
return result.value;
}
Passare un sottoinsieme di strumenti a ogni server action è importante. Un set di strumenti mirato produce decisioni AI migliori. Non dare all'AI 20 strumenti quando ne bastano 5.
Quando il pattern non funziona. Separare registro e streaming aggiunge un livello di indirezione. Per un prototipo a schermata singola con un unico strumento, questa non è architettura — è overhead. Tieni la definizione dello strumento inline finché non hai una seconda server action.
Pattern 3: Streaming con skeleton
Non mostrare mai una schermata vuota mentre l'AI genera. Mostra stati di caricamento skeleton che corrispondono alla forma dell'output atteso. La continuità visiva riduce drasticamente la latenza percepita.
// components/tool-skeleton.tsx
import { ToolName } from '@/lib/genui-registry';
const SKELETON_HEIGHTS: Record<ToolName, string> = {
metricCard: 'h-28',
dataTable: 'h-48',
barChart: 'h-64',
lineChart: 'h-64',
alertBanner: 'h-16',
};
export function ToolSkeleton({ name }: { name: ToolName }) {
return (
<div
className={`animate-pulse rounded-lg bg-muted ${SKELETON_HEIGHTS[name] ?? 'h-32'} w-full`}
aria-label="Loading..."
aria-busy="true"
/>
);
}
Per skeleton più accurati, fai corrispondere la struttura interna del componente:
export function MetricCardSkeleton() {
return (
<div className="rounded-lg border bg-card p-6">
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
<div className="mt-3 h-8 w-32 animate-pulse rounded bg-muted" />
<div className="mt-2 h-3 w-16 animate-pulse rounded bg-muted" />
</div>
);
}
Quando lo skeleton replica la struttura interna del componente, la transizione da skeleton a componente caricato è fluida: nessun layout shift, nessun flickering.
Quando il pattern non funziona. Gli skeleton personalizzati per ogni componente raddoppiano la superficie di manutenzione: ogni aggiornamento al componente richiede un aggiornamento allo skeleton. Per strumenti interni a basso traffico dove la latenza percepita non ha impatto sul business, un semplice rettangolo grigio è sufficiente. Riserva gli skeleton manuali alle superfici che gli utenti finali vedono in ogni sessione.
Pattern 4: Error boundary per la UI generata
I componenti generati falliscono in modi diversi rispetto a quelli scritti a mano. L'AI potrebbe passare una stringa numerica dove è atteso un numero, un valore negativo dove sono accettati solo positivi, o un array vuoto a un componente che richiede almeno un elemento.
Racchiudi sempre l'output generato in un error boundary:
// components/safe-genui.tsx
'use client';
import { ErrorBoundary } from 'react-error-boundary';
function GenUIFallback({ error, resetErrorBoundary }: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div className="rounded-lg border border-destructive/50 bg-destructive/5 p-4">
<p className="text-sm font-medium text-destructive">
This component could not render
</p>
<p className="mt-1 text-xs text-muted-foreground">{error.message}</p>
<button
onClick={resetErrorBoundary}
className="mt-2 text-xs underline text-muted-foreground"
>
Try again
</button>
</div>
);
}
export function SafeGenUI({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary FallbackComponent={GenUIFallback}>
{children}
</ErrorBoundary>
);
}
Racchiudi ogni pezzo di output generato con <SafeGenUI>. Un errore di rendering in un componente non deve interrompere l'intera risposta. La meccanica di base è fornita dalla libreria react-error-boundary.
Quando il pattern non funziona. L'error boundary inghiotte le eccezioni. Se non inoltri error.message a un sistema di monitoring (Sentry, GlitchTip, Datadog), lo stesso bug si ripeterà silenziosamente in produzione per settimane. Un boundary senza logging è peggio di nessun boundary, perché maschera il sintomo.
Pattern 5: Gestione dello stato per le interazioni generate
I componenti che l'AI genera spesso devono essere interattivi — una tabella ordinabile, un grafico con tooltip, un form che invia dati. Questa interattività vive nel componente stesso e non richiede considerazioni speciali.
Quello che richiede riflessione è quando la UI generata deve influenzare lo stato dell'applicazione al di fuori di sé stessa:
// Usare React context per permettere ai componenti generati di interagire con l'app
export const AppStateContext = createContext<{
onDataSelected: (data: unknown) => void;
onActionTriggered: (action: string, params: unknown) => void;
} | null>(null);
// Nel tuo componente generato
function DataTable({ columns, rows }: DataTableProps) {
const appState = useContext(AppStateContext);
function handleRowClick(row: Record<string, string>) {
appState?.onDataSelected(row);
}
return (
<table>
{/* ... */}
{rows.map((row, i) => (
<tr key={i} onClick={() => handleRowClick(row)} className="cursor-pointer hover:bg-muted">
{/* ... */}
</tr>
))}
</table>
);
}
Progetta i tuoi componenti generati con un'API chiara per le interazioni esterne. Passa le callback tramite context invece di importare direttamente lo stato globale — i componenti generati devono essere portabili.
Quando il pattern non funziona. Il coupling al context rende i componenti generati difficili da testare in isolamento: ogni storia di Storybook ora richiede il montaggio del provider. Se solo uno o due componenti hanno bisogno di stato esterno, è più onesto passare prop esplicitamente. Passa al context quando tre o più componenti condividono la stessa interfaccia di output.
Matrice di selezione dei pattern
Per un engineering manager che deve decidere quali pattern introdurre per primi, i compromessi sono i seguenti:
| Pattern | Costo di implementazione | Beneficio | Si può saltare se |
|---|---|---|---|
| Registro | 1 giorno | Scala con il catalogo; obbligatorio per la testabilità | Hai un solo strumento per sempre |
| Separazione registro/streaming | 2 ore | Riuso tra superfici; unit test isolati | Hai una sola server action |
| Skeleton | 1 giorno/componente (custom), 1 ora (universale) | Latenza percepita durante lo streaming; necessario con modelli lenti | Strumenti interni senza SLA |
| Error boundary | 2 ore + integrazione logging | Obbligatorio in produzione; senza di esso qualsiasi bug nei prop = schermata bianca | Mai — rilascia sempre |
| Stato esterno | 0,5–2 giorni | Necessario per GenUI che avvia azioni nell'app | Solo display in sola lettura |
L'error boundary è l'unica riga incondizionata. Gli altri quattro si ordinano in base alla dimensione del team: lo sviluppatore solo aggiunge gli skeleton per ultimi; un team di 5 persone rilascia il registro il primo giorno, perché il costo di coordinamento senza di esso supera il costo di costruirlo.
Costo di gestione per dimensione del team
Una stima approssimativa del costo totale di gestione su 12 mesi con inferenza a livello GPT-4o e un prodotto con traffico moderato (10.000 generazioni al giorno). Queste sono stime di primo ordine — calibra sulla tua telemetria prima di fare commit.
| Dimensione team | Sviluppo (settimane-ingegnere) | Inferenza ($/mese) | Operazioni + on-call (ore-ingegnere/mese) |
|---|---|---|---|
| Solo (indie) | 2–3 settimane | $150–$400 | 4–8 |
| Team piccolo (3–5) | 4–6 settimane | $400–$1.200 | 8–16 |
| Team medio (10+) | 8–12 settimane | $1.200–$5.000+ | 16–40 |
L'inferenza domina su scala. La leva più economica è ridurre il numero di strumenti per server action (Pattern 2) e fare caching dei prompt identici; la seconda è instradare le richieste semplici verso un modello più piccolo.
Roadmap di adozione in team
Settimane 1–2: rilascia il Pattern 4 (error boundary) e il Pattern 1 (registro) con due o tre strumenti sotto feature flag al 5% degli utenti. Settimane 3–4: aggiungi il Pattern 3 (skeleton) e il Pattern 2 (separazione); espandi al 25%. Settimane 5–8: aggiungi il Pattern 5 (stato); porta al 100%. A ogni gate, blocca il rollout finché latenza p95, percentuale di errori e costo di inferenza per sessione non rientrano negli SLO pubblicati. Non aggiungere nuovi strumenti al registro finché il primo set non è stabilizzato.
Deploy della tua app GenUI (scenario indie)
Se sei uno sviluppatore solo e vuoi rilasciare una feature GenUI nel weekend, ecco il percorso più breve e ragionevole:
- Parti da
create-next-appcon App Router. Installaai,@ai-sdk/openai,zodereact-error-boundary. - Salta il Pattern 2 nella prima versione — definisci due strumenti inline direttamente nella server action.
- Usa il rettangolo grigio universale del Pattern 3, non le versioni custom. I custom arrivano quando la feature ha utenti.
- Avvolgi lo stream in
<SafeGenUI>dal Pattern 4. Non si può saltare. - Fai deploy sul piano gratuito o Pro di Vercel. Aggiungi
OPENAI_API_KEYalle variabili d'ambiente. Il primo deploy è ungit push. - Imposta un limite di spesa rigido sulla chiave OpenAI (la dashboard OpenAI supporta limiti mensili), così un loop non svuota il budget in una notte.
Stima dei costi per un progetto hobby (1.000 generazioni al mese): circa $5–$15 di inferenza, $0 di hosting sul piano hobby Vercel, $0 di monitoring con i log integrati di Vercel. La prima bolletta significativa arriva intorno alle 50.000 generazioni al mese; è lì che il Pattern 2 (separazione delle server action) e il caching dei prompt iniziano a ripagare.
Un registro semplificato per volumi indie — server action con un unico strumento e skeleton, pronto da copiare:
// app/actions.tsx
'use server'
import { streamUI } from 'ai/rsc'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
import { MetricCard } from '@/components/metric-card'
import { Skeleton } from '@/components/skeleton'
const metricSchema = z.object({
value: z.number().describe('valore numerico corrente della metrica'),
label: z.string().describe('nome leggibile della metrica'),
delta: z.number().describe('variazione rispetto al periodo precedente, in percentuale'),
})
export async function generateUI(prompt: string) {
const result = await streamUI({
model: openai('gpt-4o-mini'),
prompt,
tools: {
metricCard: {
description: 'Mostra una singola metrica chiave con delta',
parameters: metricSchema,
generate: async function* (p: z.infer<typeof metricSchema>) {
yield <Skeleton className="h-28 rounded bg-muted" />
return <MetricCard {...p} period="vs last month" />
},
},
},
})
return result.value
}
E la chiamata lato client in un unico form:
// app/page.tsx
'use client'
import { useState } from 'react'
import { generateUI } from './actions'
export default function Page() {
const [ui, setUI] = useState<React.ReactNode>(null)
return (
<form action={async (formData) => {
setUI(await generateUI(formData.get('q') as string))
}}>
<input name="q" />
<button>Genera</button>
<div>{ui}</div>
</form>
)
}
Passa al set completo di pattern quando la feature ha utenti paganti o il catalogo di strumenti cresce fino a tre o più.
Errori comuni
Troppi strumenti. Se dai all'AI 50 componenti tra cui scegliere, farà scelte sbagliate. Ho visto team iniziare con 20+ strumenti, per poi scoprire che l'AI sceglie sistematicamente quelli sbagliati. Inizia con 5–8 strumenti ben definiti ed espandi solo sulla base di dati che mostrano quali query non trovano corrispondenza.
Descrizioni vaghe. "Display data" non è una descrizione utile per uno strumento. "Display tabular data with sortable columns when showing lists of items with multiple attributes" dice all'AI esattamente quando usarlo.
Nessun fallback. Quando il modello AI è irraggiungibile o restituisce un errore, gli utenti non vedono nulla. Prevedi sempre una UI statica di fallback per i percorsi critici. Se usi la Generative UI per una dashboard dati, crea una vista statica di default da mostrare quando l'AI non è disponibile.
Saltare la validazione Zod. L'AI passerà occasionalmente prop inattesi — una stringa dove è atteso un numero, un null dove è richiesto un valore. La validazione Zod rigorosa li intercetta prima che raggiungano il tuo componente.
Sovra-generare. Non ogni interazione richiede la Generative UI. Se un componente statico funziona, usalo. La GenUI aggiunge 200–800 ms di latenza e costa denaro. Usala per le interazioni dove la variabilità è genuinamente preziosa.
Non registrare le chiamate agli strumenti. Senza registrare quali strumenti l'AI seleziona e quali parametri passa, non hai dati per migliorare. Registra tutto dal primo giorno. I pattern che osserverai dopo una settimana di utilizzo cambieranno il modo in cui scrivi le descrizioni degli strumenti.
Checklist per la produzione
Prima di rilasciare la Generative UI in produzione:
- Tutti i componenti generati racchiusi in error boundary
- Stati di caricamento skeleton per ogni strumento
- Fallback statico quando l'AI non è disponibile o restituisce un errore
- Validazione Zod rigorosa su tutti i parametri degli strumenti
- Logging delle chiamate agli strumenti attivo (nome dello strumento, parametri, latenza)
- Monitoraggio della latenza (alert se >2 s al primo componente)
- Tracciamento dei costi per inferenza AI
- Verifica di accessibilità di tutte le composizioni di componenti generati
- Test di responsività mobile dei layout generati
- Rate limiting sulla server action
Una nota sul testing
Il testing della Generative UI richiede un approccio diverso rispetto al testing UI tradizionale. In sintesi:
- Testa i tuoi componenti in isolamento con i normali test unitari — sono semplicemente componenti React
- Testa i tuoi schema Zod separatamente per assicurarti che accettino input validi e rifiutino quelli non validi
- Per i test di integrazione contro l'AI, verifica proprietà strutturali (strumento corretto chiamato, parametri validi) non contenuti esatti (la temperatura è 22°)
- Mocka l'AI in CI ed esegui i test di integrazione AI reali di notte
Questo argomento merita un articolo dedicato. Per ora, i pattern di validazione e gestione degli errori che rendono i test affidabili sono trattati in Creare Generative UI con Vercel AI SDK.
Alternative considerate
I pattern sopra presuppongono Vercel AI SDK con React Server Components. Due alternative da conoscere prima di fare commit:
- Tambo / catalogo componenti come servizio. Framework open source per UI React generata dall'AI (
github.com/tambo-ai/tambo, ~11k star a maggio 2026) — si rilascia più velocemente (non bisogna scrivere il codice del registro) e centralizza la qualità delle descrizioni. Indicato quando la velocità fino alla prima demo conta più del costo unitario a lungo termine. - Protocolli JSON dichiarativi come Thesys C1 (API chiusa) o A2UI v0.9 (specifica aperta Google, novembre 2025) disaccoppiano il modello da React; qualsiasi client (web, mobile, vocale) può renderizzare lo stesso payload. Indicato quando hai superfici non-web, al costo di un renderer personalizzato.
- JSON puro + dispatcher manuale. Nessun SDK. Scrivi uno switch sui nomi degli strumenti. L'opzione più economica a volumi ridotti, la più difficile da mantenere dopo cinque strumenti.
L'asse di scelta è solidità e portabilità contro tempo al rilascio. Per la maggior parte dei prodotti React-only vince il percorso via SDK descritto in questo articolo; per prodotti multi-superficie o vendor-neutral, valuta A2UI.
Ulteriori letture
- Cos'è la Generative UI? — introduzione al concetto
- Creare Generative UI con Vercel AI SDK — validazione e pattern di produzione
- Vercel AI SDK: documentazione streamUI — riferimento ufficiale
- Documentazione Zod — per il layer di validazione
- react-error-boundary — per il Pattern 4
Stai lavorando su un'implementazione React di Generative UI? Ottieni una guida esperta su architettura, performance e preparazione alla produzione.
Alex
Ingegnere e Consulente Generative UI
Ingegnere senior specializzato in interfacce AI e sistemi Generative UI. Aiuta i team di prodotto a rilasciare più velocemente con il giusto stack GenUI.
Articoli correlati
Κατασκευάζοντας το Πρώτο σας Generative UI με το Vercel AI SDK
Βήμα-βήμα οδηγός για τη δημιουργία της πρώτης σας AI-powered διεπαφής με streaming συστατικά.
Προσβασιμότητα σε Generative UI: Δημιουργία Συμπεριληπτικών AI Διεπαφών
Πρακτικός οδηγός για προσβάσιμα γεννητικά interfaces — screen readers, πλοήγηση με πληκτρολόγιο και συνδυαστικά προβλήματα προσβασιμότητας.
CopilotKit vs Vercel AI SDK vs Thesys: Σύγκριση Frameworks
Μια ειλικρινής σύγκριση των τριών κύριων frameworks Generative UI, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
Resta aggiornato su Generative UI
Articoli settimanali, aggiornamenti sui framework e guide pratiche di implementazione — direttamente nella tua casella di posta.
Hai bisogno di aiuto per implementare quello che hai appena letto?
Prenota una consulenza gratuita