React 19.3: la release para borrar código, no para escribirlo
Hace un mes revisé el repo de un cliente. Una app de e-commerce, React, tres años de vida, gente competente detrás.
Busqué isMounted en el proyecto. Diecinueve resultados. Busqué wrapperRef. Once. Y en el package.json, una librería de animación de 40 KB que solo se usaba para hacer un fade entre dos pantallas.
Ninguna de las tres es un error. Son workarounds: código que existe porque React no daba la primitiva y alguien lo resolvió con lo que había.
React 19.3 salió estable en npm el 9 de septiembre de 2026 y es, sobre todo, una release para borrar ese tipo de código. El problema de un workaround es que se queda para siempre, y cada dev nuevo del equipo asume que así es como se hace.
No trae un paradigma nuevo. Trae dos APIs que salen de experimental —View Transitions y Fragment Refs—, una nueva, browser(), y un ajuste en Server Components. Cada una sustituye un apaño concreto que llevas años arrastrando. Vamos una por una, con lo que se borra en cada caso.
En resumen: React 19.3 (react@19.3.0 y react-dom@19.3.0, publicados en npm el 9 de septiembre de 2026) estabiliza View Transitions y Fragment Refs, estrena browser() en react-dom, permite renderizar un Context directamente desde un Server Component y mejora el soporte de Trusted Types. No documenta ningún breaking change.
| Cambio | Estado en 19.3 | Se importa de | Qué código elimina |
|---|---|---|---|
<ViewTransition> |
Estable (era experimental) | react |
Librería de animación para transiciones de página y enter/exit de listas |
addTransitionType() |
Nueva | react |
Estado de dirección pasado por props hasta el componente animado |
Fragment con ref |
Estable (era experimental) | react |
El <div> wrapper que solo existía para colgar un ref, y el cloneElement |
browser() |
Nueva | react-dom |
El trío useState + useEffect + flag mounted |
| Context en Server Components | Nuevo | — | El componente 'use client' que solo renderizaba el Provider |
| Trusted Types | Nuevo | react-dom |
Sanitizado manual de TrustedHTML / TrustedScript |
View Transitions en React 19.3: borra la librería de animación
<ViewTransition> ya no es experimental. Se importa de react y envuelve el trozo del árbol que quieres animar.
import { ViewTransition, useState, startTransition } from 39;react39;;
export default function Component() {
const [showItem, setShowItem] = useState(false);
return (
<>
<button => { startTransition(() => { setShowItem(prev => !prev); }); }}>
{showItem ? 39;➖39; : 39;➕39;}
</button>
{showItem && (
<ViewTransition>
<Video video={videos[0]} />
</ViewTransition>
)}
</>
);
}
No hay animate, no hay variants, no hay AnimatePresence. Envuelves y ya. Por debajo React se apoya en la View Transition API del navegador.
Hay cuatro formas de activarlo, según lo que pase en el árbol:
- enter — se añade un
ViewTransitional árbol. - exit — se elimina.
- update — cambian sus hijos, sea contenido o
style. - share — un
ViewTransitioncon nombre desaparece en un sitio y aparece en otro.
Cada tipo tiene su prop del mismo nombre —enter, exit, update, share— y su event prop: onEnter, onExit, onUpdate, onShare. Y existe además default, que fija la animación de los tipos que no declares: es lo que te permite apagarlos todos de golpe con default="none".
Y ahora la regla que te va a costar media hora si no la lees: un setState normal no dispara la animación. Solo se activa dentro de una Transition: startTransition, useTransition, useDeferredValue o una navegación de Suspense.
No es un detalle de implementación, es la decisión de diseño central: React no anima por si acaso, anima cuando tú marcas el cambio como transición. Si tu <ViewTransition> no hace nada, mira el setState antes que el CSS.
Segundo detalle: la direccionalidad. El caso clásico —un carrusel que debe animar distinto según vayas hacia delante o hacia atrás— se resuelve con addTransitionType, otra función nueva de react:
import { addTransitionType, startTransition } from 39;react39;;
function nextSlide() {
startTransition(() => {
addTransitionType(39;next39;);
setCurrentSlide(c => c + 1);
});
}
Etiquetas la transición y luego la consumes en CSS con :active-view-transition-type(next), o mapeas tipo → animación directamente en el componente:
<ViewTransition
enter={{ 39;next39;: 39;from-right39;, 39;previous39;: 39;from-left39; }}
exit={{ 39;next39;: 39;to-left39;, 39;previous39;: 39;to-right39; }}
>
<Page />
</ViewTransition>
Ese mapa { tipo: animación } sustituye al estado de dirección que antes mantenías, pasabas por props hasta el componente animado y traducías a variants. Ahora vive donde ocurre la navegación.
Y cuando dentro hay un Suspense, la doc recomienda animar solo el cambio de fallback a contenido y no todo lo que se mueva por debajo:
<ViewTransition update="auto" default="none">
<Suspense fallback={<Fallback />}>
<Component />
</Suspense>
</ViewTransition>
Qué borras: las transiciones de página y los enter/exit de listas y modales. Ojo, no digo que desinstales tu librería de animación mañana: los gestos, los drags, los springs físicos y las animaciones interrumpibles siguen siendo territorio de Framer Motion o GSAP. Pero si la instalaste para hacer un fade entre rutas —que es la mayoría de los casos que veo en revisiones— ya no la necesitas.
Fragment Refs en React 19.3: un ref sin el <div> wrapper
Fragment Refs permiten pasar un ref a un <Fragment> y recibir un FragmentInstance que opera sobre los hijos de primer nivel sin añadir ningún nodo al DOM. Son estables desde React 19.3.
Es el cambio más pequeño de la release y probablemente el que más veces al mes te va a ahorrar un mal rato.
Fragment ahora acepta ref. Te devuelve un FragmentInstance que opera sobre los hijos de primer nivel, sin meter un nodo extra en el DOM.
Los métodos disponibles son estos:
| Categoría | Métodos |
|---|---|
| Eventos | addEventListener, removeEventListener, dispatchEvent |
| Foco | focus() (en profundidad, depth-first), focusLast(), blur() |
| Observers | observeUsing(observer), unobserveUsing(observer) |
| Layout y DOM | getClientRects(), getRootNode(), compareDocumentPosition(otherNode), scrollIntoView(options) |
Un componente InView que antes exigía envolver los hijos en un div —con el consiguiente estropicio si el padre era un grid o un flex— ahora se escribe así:
import { Fragment, useRef, useLayoutEffect } from 39;react39;;
export default function InView({ onChange, children }) {
const fragmentRef = useRef(null);
useLayoutEffect(() => {
const visibleElements = new Set();
const observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) visibleElements.add(e.target);
else visibleElements.delete(e.target);
});
onChange(visibleElements.size > 0);
});
const fragmentInstance = fragmentRef.current;
fragmentInstance.observeUsing(observer);
return () => { fragmentInstance.unobserveUsing(observer); };
}, [onChange]);
return <Fragment ref={fragmentRef}>{children}</Fragment>;
}
Fíjate en observeUsing: le pasas el observer y él se encarga de suscribir a cada hijo. No iteras children, no clonas elementos, no pides refs a los hijos.
Para accesibilidad, mover el foco al primer elemento enfocable de un grupo se queda en dos líneas:
import { Fragment, useRef, useEffect } from 39;react39;;
function Component() {
const fragmentRef = useRef(null);
useEffect(() => {
const fragmentInstance = fragmentRef.current;
fragmentInstance.focus();
}, []);
return (
<Fragment ref={fragmentRef}>
{posts.map(post => (
<Heading key={post.id}>{post.title}</Heading>
))}
</Fragment>
);
}
Qué borras: los wrappers de layout que no pintan nada y el cloneElement con ref que usabas para llegar a los hijos. Es el mismo tipo de deuda que genera el props drilling en React: estructura que existe para transportar algo, no para representar nada.
use(browser()) en React 19.3: borra el flag mounted
browser() es una función nueva de react-dom que, consumida con use(browser()), suspende en el servidor y no suspende en el cliente: marca un componente como client-only sin useState ni useEffect.
Es el cambio más discreto de la release y el que más código muerto elimina en una app con SSR.
El patrón que todos hemos escrito mil veces: un componente necesita window, Intl, localStorage o cualquier cosa que solo existe en el navegador, así que montas el ritual de useState(false) + useEffect(() => setMounted(true), []) + if (!mounted) return null.
React 19.3 mete browser() en react-dom. Se consume con use() y su comportamiento cabe en una línea: suspende en el servidor y no suspende en el cliente.
import { Suspense, use } from 39;react39;;
import { browser } from 39;react-dom39;;
function TimeZone() {
use(browser());
const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
return <p>{timeZone}</p>;
}
export default function App() {
return (
<>
<p>Your current time zone is:</p>
<Suspense fallback="Loading...">
<TimeZone />
</Suspense>
</>
);
}
Tres líneas de estado sustituidas por una. Y el fallback deja de ser null para pasar a ser un Suspense de verdad, que es lo que debería haber sido siempre.
Además se puede llamar condicionalmente, cosa que no es habitual en las APIs de React:
function TimeZone({ defaultValue }) {
if (defaultValue) return <p>{defaultValue}</p>;
use(browser());
const localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
return <p>{localTimeZone}</p>;
}
Y dentro de tus propios hooks, que es donde se pone interesante:
function useBrowserQuery(query, options) {
if (options.initialData === undefined) use(browser());
return useQuery(query, options);
}
Ahí acabas de mover una decisión de renderizado —"esto solo puede resolverse en cliente"— del componente a la capa de datos. El componente ya no sabe nada del entorno.
Requisito: necesitas un Suspense por encima. Sin él no funciona. Si todavía tratas Suspense como un spinner y no como una herramienta de orquestación, aquí lo desarrollo: fetching paralelo en Next.js con Suspense y Promise.all.
Context en Server Components: borra el Provider intermedio
Desde React 19.3, un Server Component puede importar un Context definido en un módulo 'use client' y renderizarlo directamente, sin un componente Provider intermedio.
Si trabajas con RSC conoces el peaje. Para pasar datos del servidor al árbol de cliente había que crear un componente 'use client' cuya única razón de existir era renderizar el Provider:
// user-context.js ('use client')
export const UserContext = createContext(null);
export function UserProvider({ currentUser, children }) {
return <UserContext value={currentUser}>{children}</UserContext>;
}
// server-component.js
import { UserProvider } from 39;./user-context39;;
export async function Layout({ children }) {
const currentUser = await getCurrentUser();
return <UserProvider currentUser={currentUser}>{children}</UserProvider>;
}
Ahora el Server Component importa el Context directamente de su módulo 'use client' y lo renderiza:
// user-context.js ('use client')
export const UserContext = createContext(null);
// server-component.js
import { UserContext } from 39;./user-context39;;
export async function Layout({ children }) {
const currentUser = await getCurrentUser();
return <UserContext value={currentUser}>{children}</UserContext>;
}
Un fichero menos y, sobre todo, una capa de indirección menos. Si estás montando esto en producción, escribí sobre las decisiones reales de React Server Components que hay detrás de esa frontera server/client: este cambio elimina uno de los puntos de fricción que mencionaba allí.
Otros cambios de React 19.3: Trusted Types, rendimiento y renombrados
Tres cosas más que no dan para post pero que conviene saber.
Trusted Types. React ya no coerciona a string los valores TrustedHTML, TrustedScript y TrustedScriptURL: los pasa tal cual para que el navegador valide la política. Si sirves con Content-Security-Policy: require-trusted-types-for 'script', esto cierra una vía de XSS basado en DOM que antes tenías que tapar tú.
Rendimiento. Dos cambios, sin cifras publicadas y sin que yo me las invente: las Transitions se renderizan de forma independiente en vez de enredarse en un único render, así que una Transition lenta ya no bloquea a otras que no tienen nada que ver; y las actualizaciones que vienen de eventos de resize se agrupan hasta el siguiente frame. Si tu app tiene layout reactivo al viewport, lo notarás sin tocar nada.
Renombrados y fixes. useActionState pasa a hablar de "action state" en vez de "form state". Y hay arreglos que probablemente expliquen algún bug que archivaste como "cosas raras": useDeferredValue quedándose con el valor viejo, useSyncExternalStore perdiendo mutaciones, useEffectEvent roto dentro de forwardRef y memo, y la propagación de context en los fallbacks de Suspense.
¿Merece la pena actualizar a React 19.3 hoy?
La nota de release oficial de React 19.3 no documenta ningún breaking change. La instalación es esta:
npm install react@19.3 react-dom@19.3
Aviso para quien llegue desde los sandboxes de la doc: ahí verás versiones 19.3.0-canary-*. Esas son del entorno de la documentación, no la instrucción de instalación. La estable es 19.3.0.
Mi recomendación: actualiza y no migres nada todavía. Primero mide cuánto código muerto tienes. Es un ejercicio de veinte minutos y te da la lista de la compra.
Yo lo hago con un agente: le pido a Claude Code que barra el repo buscando los tres patrones —flags de mounted, wrappers que solo existen para un ref y animaciones de ruta hechas con librería— y que me devuelva un inventario con ubicación y coste, no un PR. Que el agente haga el inventario y la decisión la tomes tú es justo lo que enseño en el curso Construye con IA: de la idea al producto con Claude Code.
Y si además sigues el ecosistema Angular, merece la pena comparar cómo resuelve cada framework lo mismo. Lo desarrollé en el post sobre Signals en Angular 22 frente a React 19. React 19.3 refuerza esa dirección: el modelo de reactividad no se toca, lo que mejora es qué puedes expresar sin escribir infraestructura.
Por dónde empezar a migrar a React 19.3
Abre tu proyecto y busca mounted. Solo eso.
Cada resultado es un componente que tarda un render extra en aparecer, que probablemente hace flash en producción y que existe porque React no tenía forma de decir "esto es client-only". Ahora la tiene. Ese es el mejor punto de entrada a React 19.3 porque el cambio es local, no rompe nada y se ve en el primer render.
Cuando termines con esos, ve a por los <div> wrapper. Y deja las transiciones de página para el final, que son las más divertidas y las que más tiempo te van a comer.
En Dominicode Labs estamos probando estas APIs sobre proyectos reales y compartiendo lo que funciona y lo que no. Y si prefieres verlo en vídeo, lo iré contando en el canal de Dominicode a medida que lo lleve a producción.
Preguntas frecuentes sobre React 19.3
¿React 19.3 rompe algo si actualizo desde 19.2?
La nota de release de React 19.3 no documenta ningún breaking change. La actualización es npm install react@19.3 react-dom@19.3 y las APIs nuevas son aditivas: ViewTransition y addTransitionType no estaban en la API estable —vivían en los builds experimentales—, browser() es nueva, y que Fragment acepte ref no cambia el comportamiento de los Fragment que ya tienes. Aun así, actualiza en una rama y pasa tu suite de tests: la release incluye arreglos en useDeferredValue, useSyncExternalStore y useEffectEvent, y si tu código dependía sin saberlo del comportamiento defectuoso, ahí es donde lo vas a notar.
¿Por qué mi ViewTransition no anima nada?
Casi siempre por lo mismo: el cambio de estado no está dentro de una Transition. <ViewTransition> solo se activa con startTransition, useTransition, useDeferredValue o una navegación de Suspense. Un setState normal actualiza el DOM sin animar. Antes de tocar el CSS, comprueba que el setState que provoca el cambio está envuelto en una Transition.
¿Puedo usar View Transitions de React 19.3 con Next.js o React Router?
Sí, porque <ViewTransition> es un componente de react y no depende del router. La condición es que el cambio de estado ocurra dentro de una Transition, y las navegaciones de los routers modernos ya lo hacen. Lo que no obtienes automáticamente es la direccionalidad: para animar distinto hacia delante y hacia atrás tienes que llamar tú a addTransitionType('next') dentro del startTransition que dispara la navegación.
¿Fragment Refs sustituyen a los refs normales?
No. Un ref normal apunta a un nodo del DOM y eso sigue siendo lo correcto cuando ese nodo existe. Fragment Refs resuelven el caso en el que necesitas operar sobre un conjunto de hijos y no hay un elemento común que los envuelva, o lo hay solo porque lo metiste tú para colgar el ref. El FragmentInstance que recibes trabaja sobre los hijos de primer nivel: registra eventos, mueve el foco con focus() y focusLast(), conecta un IntersectionObserver o un ResizeObserver con observeUsing(), mide con getClientRects() y hace scroll con scrollIntoView().
¿use(browser()) elimina todos los useEffect de mi app?
No, solo un patrón concreto: el de marcar un componente como client-only. browser() se importa de react-dom, suspende en el servidor y no suspende en el cliente, así que sustituye al trío useState + useEffect + flag mounted. Necesita un Suspense por encima para funcionar. Los efectos que sincronizan con sistemas externos —suscripciones, listeners, integraciones con librerías no-React— siguen siendo useEffect.
¿Necesito un framework con Server Components para aprovechar React 19.3?
Para nada. View Transitions, Fragment Refs y use(browser()) funcionan en cualquier aplicación React; browser() cobra especial sentido si haces SSR, pero no exige RSC. Lo que sí requiere una configuración con Server Components es la mejora de Context, que permite a un Server Component importar un Context definido en un módulo 'use client' y renderizarlo directamente, sin el componente Provider intermedio.
¿Cómo instalo React 19.3 y por qué veo versiones canary en la documentación?
Se instala con npm install react@19.3 react-dom@19.3. Las versiones 19.3.0-canary-* que aparecen en los sandboxes interactivos de react.dev pertenecen al entorno de la propia documentación y no son la instrucción de instalación. En npm, react@19.3.0 y react-dom@19.3.0 son estables desde el 9 de septiembre de 2026.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
