React 19.3 salió el 9 de septiembre y trae las View Transitions como componente estable. Si alguna vez escribiste CSS transition a mano para que un elemento apareciera y desapareciera sin brusquedad, esto te interesa: ahora le pides al navegador la animación y React decide sola cuándo entra, sale, se mueve o se actualiza cada cosa. En esta guía vas a construir dos casos reales con View Transitions en React 19.3: una lista que anima sus elementos y una galería que cambia de diapositiva según la dirección.
Qué necesitas antes de empezar
Antes de escribir código, actualiza a la versión nueva y verifica que tu navegador soporta la API sobre la que todo esto se apoya:
npm install react@19.3 react-dom@19.3
Las View Transitions de React usan la View Transition API del navegador, así que necesitas un navegador que la implemente — Chrome la tiene completa; consulta el soporte actual en caniuse.com antes de darla por hecha en producción. Es una API del DOM: en React Native todavía no funciona, está en camino.
Y la regla que más confusiones evita: una animación solo ocurre si el cambio de estado está marcado como Transition. Sin startTransition, no hay animación. Esto es lo primero que revisa todo el mundo cuando «no funciona».
Cómo funcionan las View Transitions en React 19.3
Envuelves lo que quieres animar en un componente ViewTransition y React clasifica sola lo que pasó:
import { ViewTransition, useState, startTransition } from 'react';
function App() {
const [showItem, setShowItem] = useState(false);
return (
<>
<button onClick={() => {
startTransition(() => {
setShowItem((prev) => !prev);
});
}}>
Mostrar u ocultar
</button>
{showItem && (
<ViewTransition>
<MiTarjeta />
</ViewTransition>
)}
</>
);
}
React elige el tipo de animación según lo que cambió en el árbol:
- enter: el
ViewTransitionse añadió al árbol — el contenido aparece. - exit: se quitó del árbol — el contenido desaparece.
- update: el contenido o sus estilos cambiaron sin entrar ni salir.
- share: un
ViewTransitioncon nombre se quitó de un lugar y apareció en otro — el elemento «viaja».
Por defecto todo se resuelve con un cross-fade suave. Si quieres otra cosa, defines una View Transition Class en CSS o usas los props onEnter, onExit, onUpdate y onShare con la Web Animations API.
Tu primera animación de lista
Vamos al caso completo: una lista donde cada elemento aparece con fade y el botón dispara todo dentro de un startTransition.
import { ViewTransition, useState, startTransition } from 'react';
export function ListaDeTareas() {
const [tareas, setTareas] = useState([
{ id: 1, texto: 'Revisar el pull request' },
{ id: 2, texto: 'Subir el deploy del viernes' },
]);
const [nueva, setNueva] = useState('');
function agregar() {
startTransition(() => {
setTareas((prev) => [
...prev,
{ id: Date.now(), texto: nueva },
]);
});
setNueva('');
}
return (
<div>
<input
value={nueva}
onChange={(e) => setNueva(e.target.value)}
placeholder="Nueva tarea"
/>
<button onClick={agregar}>Agregar</button>
<ul>
{tareas.map((tarea) => (
<ViewTransition key={tarea.id}>
<li>{tarea.texto}</li>
</ViewTransition>
))}
</ul>
</div>
);
}
Cómo comprobar que funcionó: escribe algo, dale agregar y observa que el elemento nuevo aparece con un fade en vez de saltar. Si aparece instantáneo, revisa que el setTareas esté dentro de startTransition — es el error más común.
Animaciones según la causa con addTransitionType
El cross-fade está bien para empezar, pero el caso interesante es cuando la misma acción se anima distinto según por qué pasó. Una galería que avanza debería deslizar hacia la izquierda; la misma galería retrocediendo debería deslizar hacia la derecha. Eso se resuelve con addTransitionType:
import {
ViewTransition,
useState,
startTransition,
addTransitionType,
} from 'react';
function siguiente() {
startTransition(() => {
addTransitionType('next');
setIndice((i) => i + 1);
});
}
function anterior() {
startTransition(() => {
addTransitionType('previous');
setIndice((i) => i - 1);
});
}
export function Galeria({ fotos }) {
const [indice, setIndice] = useState(0);
const foto = fotos[indice];
return (
<>
<button onClick={anterior}>Anterior</button>
<button onClick={siguiente}>Siguiente</button>
<ViewTransition
key={foto.id}
enter={{
next: 'desde-derecha',
previous: 'desde-izquierda',
}}
exit={{
next: 'hacia-izquierda',
previous: 'hacia-derecha',
}}
>
<img src={foto.url} alt={foto.alt} />
</ViewTransition>
</>
);
}
Y las clases de animación viven en tu CSS, donde el tipo de transición queda disponible como pseudo-clase:
::view-transition-new(desde-derecha) {
animation: entrarDesdeDerecha 250ms ease-out;
}
::view-transition-old(hacia-izquierda) {
animation: salirHaciaIzquierda 250ms ease-in;
}
Los tipos también llegan al navegador como view transition types, así que puedes usar :active-view-transition-type(...) en CSS para apuntar a una transición concreta.
ViewTransition junto a Suspense
El patrón que recomienda el equipo de React para datos asíncronos: el fallback aparece sin animación (debe ser inmediato), y solo la actualización de fallback a contenido se anima:
<ViewTransition update="auto" default="none">
<Suspense fallback={<Cargando />}>
<Perfiles />
</Suspense>
</ViewTransition>
Ese update="auto" default="none" dice explícito lo que quieres: la entrada del fallback no se anima, la actualización sí. Sin este parámetro corres el riesgo de que el fallback aparezca con fade y la interfaz se sienta lenta cuando en realidad está cargando rápido.
Lo demás que llega en 19.3
Tres adiciones más que valen la pena conocer, sin tutorial completo aquí:
Fragment Refs. Ahora puedes pasar un ref a un Fragment y operar sobre los hijos como grupo, sin un elemento envolvente que ensucie el DOM:
import { Fragment, useRef, useEffect } from 'react';
function ListaDeEncabezados({ posts }) {
const ref = useRef(null);
useEffect(() => {
ref.current.focus();
}, []);
return (
<Fragment ref={ref}>
{posts.map((post) => (
<h2 key={post.id}>{post.titulo}</h2>
))}
</Fragment>
);
}
El API browser(). El reemplazo de primera clase para el clásico typeof window !== 'undefined': el componente suspende en el servidor y renderiza normal en el cliente:
import { use } from 'react';
import { browser } from 'react-dom';
function ZonaHoraria({ valorPorDefecto }) {
if (valorPorDefecto) {
return <p>{valorPorDefecto}</p>;
}
use(browser());
const zona = new Intl.DateTimeFormat().resolvedOptions().timeZone;
return <p>{zona}</p>;
}
A diferencia de los hooks, use sí puede llamarse en un condicional.
Contexto en Server Components. Un Server Component puede renderizar directamente un Context importado de un módulo 'use client', sin el componente Provider envolviendo. Los Server Components siguen sin poder crear Context, solo renderizarlo.
Errores frecuentes
- No se anima nada. El cambio de estado no está dentro de
startTransition. Es la causa número uno y la primera que revisa. - Funciona en mi navegador pero no en todos. La View Transition API es del navegador; sin soporte, no hay animación — el contenido aparece y desaparece como siempre, no rompe nada, pero tampoco anima.
- Mis efectos se ejecutan dos veces en hidratación. No es un bug tuyo: en 19.3 el Strict Mode durante la hidratación ahora invoca los efectos dos veces, igualando lo que ya pasaba en render del cliente. Revisa que tus efectos sean idempotentes.
- Las transiciones lentas bloqueaban a las demás. Ya no: en 19.3 las transiciones renderizan de forma independiente y una lenta no detiene a las demás.
Fuente original: el anuncio de React 19.3 en react.dev, con la referencia completa del componente en la documentación de ViewTransition.

