React 19 ha sido lanzado oficialmente, añadiendo muchas nuevas características como Server Actions, nuevos hooks y mejoras significativas en el manejo de formularios. Este artículo explica las principales novedades de React 19 con ejemplos de código prácticos.
Principales novedades de React 19
Resumen
flowchart TB
subgraph React19["React 19"]
subgraph Hooks["Nuevos Hooks"]
H1["use() - Lectura de Promise/Context"]
H2["useActionState() - Estado de acciones de formulario"]
H3["useFormStatus() - Estado de envío de formulario"]
H4["useOptimistic() - Actualización optimista"]
end
subgraph Actions["Actions"]
A1["Server Actions - Funciones del lado del servidor"]
A2["Client Actions - Procesamiento asíncrono del cliente"]
A3["Integración con formularios - form action"]
end
subgraph Others["Otras mejoras"]
O1["ref as prop - forwardRef innecesario"]
O2["Document Metadata - Escritura directa de title, etc."]
O3["Gestión de Stylesheet - Control de orden con precedence"]
O4["Resource Preloading - API prefetch/preload"]
end
end
Hook use()
Lectura de Promises
use() es un nuevo hook para leer Promises o Context durante el renderizado.
// use() - Lectura de Promises
import { use, Suspense } from 'react';
// Función de fetch de datos
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Componente
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
// Usar con Suspense
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Componente padre
function UserPage({ userId }: { userId: string }) {
// Pasar Promise como props
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<div>Cargando usuario...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Se puede llamar use() condicionalmente (diferencia con otros hooks)
function ConditionalData({ shouldFetch, dataPromise }: {
shouldFetch: boolean;
dataPromise: Promise<Data>;
}) {
if (!shouldFetch) {
return <div>No se necesitan datos</div>;
}
// Se puede usar después de condiciones
const data = use(dataPromise);
return <div>{data.value}</div>;
}
Lectura de Context
// use() - Lectura de Context
import { use, createContext } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
function ThemedButton() {
// Se puede usar use() en lugar de useContext()
const theme = use(ThemeContext);
return (
<button className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
Haz clic
</button>
);
}
// Lectura condicional de Context
function ConditionalTheme({ useTheme }: { useTheme: boolean }) {
if (!useTheme) {
return <button>Botón predeterminado</button>;
}
// Se puede usar después de condiciones
const theme = use(ThemeContext);
return <button className={`theme-${theme}`}>Botón con tema</button>;
}
Actions
Server Actions
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Server Action
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validación
if (!title || title.length < 3) {
return { error: 'El título debe tener al menos 3 caracteres' };
}
// Guardar en base de datos
const post = await db.post.create({
data: { title, content },
});
// Revalidar caché
revalidatePath('/posts');
// Redireccionar
redirect(`/posts/${post.id}`);
}
// Acción de actualización
export async function updatePost(id: string, formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.update({
where: { id },
data: { title, content },
});
revalidatePath(`/posts/${id}`);
return { success: true };
}
// Acción de eliminación
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath('/posts');
redirect('/posts');
}
Integración con formularios
// app/posts/new/page.tsx
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions';
export default function NewPostPage() {
// useActionState - Gestión de estado de acciones de formulario
const [state, formAction, isPending] = useActionState(
createPost,
{ error: null }
);
return (
<form action={formAction}>
<div>
<label htmlFor="title">Título</label>
<input
id="title"
name="title"
required
disabled={isPending}
/>
</div>
<div>
<label htmlFor="content">Contenido</label>
<textarea
id="content"
name="content"
disabled={isPending}
/>
</div>
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Publicando...' : 'Publicar'}
</button>
</form>
);
}
useFormStatus
// Obtener estado de envío de formulario
import { useFormStatus } from 'react-dom';
function SubmitButton() {
// Obtener estado de envío del <form> padre
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? (
<>
<Spinner />
Enviando...
</>
) : (
'Enviar'
)}
</button>
);
}
// Uso en formulario
function ContactForm() {
async function submitForm(formData: FormData) {
'use server';
// Procesamiento de envío
}
return (
<form action={submitForm}>
<input name="email" type="email" required />
<textarea name="message" required />
{/* SubmitButton obtiene automáticamente el estado del form padre */}
<SubmitButton />
</form>
);
}
useOptimistic - Actualización optimista
// Implementación de actualización optimista
import { useOptimistic, startTransition } from 'react';
interface Message {
id: string;
text: string;
sending?: boolean;
}
function ChatMessages({ messages }: { messages: Message[] }) {
// Gestión de estado optimista
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: Message) => [
...state,
{ ...newMessage, sending: true },
]
);
async function sendMessage(formData: FormData) {
const text = formData.get('message') as string;
const tempId = crypto.randomUUID();
// Agregar optimistamente
startTransition(() => {
addOptimisticMessage({
id: tempId,
text,
sending: true,
});
});
// Enviar al servidor
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify({ text }),
});
}
return (
<div>
<ul>
{optimisticMessages.map((message) => (
<li
key={message.id}
className={message.sending ? 'opacity-50' : ''}
>
{message.text}
{message.sending && <span> (Enviando...)</span>}
</li>
))}
</ul>
<form action={sendMessage}>
<input name="message" required />
<button type="submit">Enviar</button>
</form>
</div>
);
}
Ejemplo de botón de like
// Botón de like optimista
function LikeButton({ postId, initialLiked, initialCount }: {
postId: string;
initialLiked: boolean;
initialCount: number;
}) {
const [{ liked, count }, setOptimistic] = useOptimistic(
{ liked: initialLiked, count: initialCount },
(state, newLiked: boolean) => ({
liked: newLiked,
count: state.count + (newLiked ? 1 : -1),
})
);
async function toggleLike() {
const newLiked = !liked;
// Actualizar optimistamente
startTransition(() => {
setOptimistic(newLiked);
});
// Enviar al servidor
await fetch(`/api/posts/${postId}/like`, {
method: newLiked ? 'POST' : 'DELETE',
});
}
return (
<button onClick={toggleLike}>
{liked ? '❤️' : '🤍'} {count}
</button>
);
}
ref as prop
En React 19, puedes recibir ref como props sin necesidad de forwardRef.
// React 18 y anteriores: forwardRef necesario
const InputOld = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
// React 19: ref se puede pasar como prop normal
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// Ejemplo de uso
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<form>
<Input ref={inputRef} placeholder="Ingresa texto" />
<button
type="button"
onClick={() => inputRef.current?.focus()}
>
Enfocar
</button>
</form>
);
}
Document Metadata
Ahora puedes escribir metadatos directamente dentro de los componentes.
// Escritura directa de metadatos
function BlogPost({ post }: { post: Post }) {
return (
<article>
{/* Se eleva automáticamente al head del documento */}
<title>{post.title} - Mi Blog</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// Uso en múltiples páginas
function ProductPage({ product }: { product: Product }) {
return (
<div>
<title>{product.name} | Mi Tienda</title>
<meta name="description" content={product.description} />
{/* Datos estructurados */}
<script type="application/ld+json">
{JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
price: product.price,
})}
</script>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
Gestión de hojas de estilo
// Gestión de prioridad de hojas de estilo
function ComponentWithStyles() {
return (
<>
{/* Controlar orden de carga con precedence */}
<link
rel="stylesheet"
href="/styles/base.css"
precedence="default"
/>
<link
rel="stylesheet"
href="/styles/components.css"
precedence="default"
/>
<link
rel="stylesheet"
href="/styles/utilities.css"
precedence="high"
/>
<div className="styled-component">
Contenido
</div>
</>
);
}
// Hoja de estilo dinámica
function ThemeSwitcher({ theme }: { theme: 'light' | 'dark' }) {
return (
<>
<link
rel="stylesheet"
href={`/themes/${theme}.css`}
precedence="high"
/>
<div>Contenido con tema</div>
</>
);
}
Precarga de recursos
// API de precarga de recursos
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
function ResourceHints() {
// Pre-resolución DNS
prefetchDNS('https://api.example.com');
// Pre-conexión
preconnect('https://cdn.example.com');
// Precarga de recursos
preload('/fonts/custom.woff2', {
as: 'font',
type: 'font/woff2',
crossOrigin: 'anonymous',
});
// Pre-inicialización de scripts
preinit('/scripts/analytics.js', {
as: 'script',
});
return <div>Contenido</div>;
}
// Precarga de imágenes
function ImageGallery({ images }: { images: string[] }) {
// Precargar las siguientes imágenes
useEffect(() => {
images.slice(1, 4).forEach((src) => {
preload(src, { as: 'image' });
});
}, [images]);
return (
<div>
{images.map((src) => (
<img key={src} src={src} alt="" />
))}
</div>
);
}
React Compiler (experimental)
// Memoización automática con React Compiler
// Antes: Se necesitaban useMemo/useCallback manuales
function ProductListOld({ products, onSelect }: Props) {
const sortedProducts = useMemo(
() => [...products].sort((a, b) => a.price - b.price),
[products]
);
const handleSelect = useCallback(
(id: string) => onSelect(id),
[onSelect]
);
return (
<ul>
{sortedProducts.map((product) => (
<ProductItem
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</ul>
);
}
// Después: React Compiler memoiza automáticamente
function ProductList({ products, onSelect }: Props) {
// No se necesita memoización manual - el compilador optimiza
const sortedProducts = [...products].sort((a, b) => a.price - b.price);
return (
<ul>
{sortedProducts.map((product) => (
<ProductItem
key={product.id}
product={product}
onSelect={(id) => onSelect(id)}
/>
))}
</ul>
);
}
// Habilitar React Compiler en babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// opciones
}],
],
};
Mejoras en manejo de errores
// Visualización de errores mejorada
// Visualización detallada de errores de hydration
// React 19 muestra las diferencias específicas
// Mejoras en Error Boundary
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
// React 19: Stack trace más detallado
console.error('Error:', error);
console.error('Component Stack:', info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Ejemplo de uso
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<MainContent />
</ErrorBoundary>
);
}
Guía de migración
Migración de React 18 a 19
# Actualizar paquetes
npm install react@19 react-dom@19
# Definiciones de tipos TypeScript
npm install -D @types/react@19 @types/react-dom@19
// Cambios principales
// 1. forwardRef → prop normal
// Antes
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
<input ref={ref} {...props} />
));
// Después
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// 2. useContext → use (opcional)
// Antes
const theme = useContext(ThemeContext);
// Después (conveniente para uso condicional)
const theme = use(ThemeContext);
// 3. APIs obsoletas eliminadas
// - defaultProps (componentes de función)
// - propTypes
// - createFactory
// - render (react-dom)
// Alternativa a defaultProps
// Antes
function Button({ size = 'medium' }) { ... }
Button.defaultProps = { size: 'medium' };
// Después (usar parámetros por defecto)
function Button({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) {
// ...
}
Resumen
React 19 proporciona muchas nuevas características que mejoran significativamente la experiencia del desarrollador.
Principales novedades
| Característica | Uso |
|---|---|
| use() | Lectura de Promise/Context |
| useActionState | Gestión de estado de acciones de formulario |
| useFormStatus | Obtener estado de envío |
| useOptimistic | Actualización optimista |
| Server Actions | Procesamiento del lado del servidor |
| ref as prop | Sin necesidad de forwardRef |
Recomendaciones de migración
- Nuevos proyectos: Adoptar React 19
- Proyectos existentes: Migrar gradualmente
- Server Actions: Combinar con Next.js 14+
Con React 19, el desarrollo de aplicaciones React más intuitivo y rápido es posible.