{"id":19,"date":"2026-09-15T10:14:02","date_gmt":"2026-09-15T08:14:02","guid":{"rendered":"https:\/\/desarrollosms.com\/?p=19"},"modified":"2026-09-16T00:39:22","modified_gmt":"2026-09-15T22:39:22","slug":"react-19-3-view-transitions","status":"publish","type":"post","link":"https:\/\/desarrollosms.com\/index.php\/2026\/09\/15\/react-19-3-view-transitions\/","title":{"rendered":"React 19.3 &#8211; c\u00f3mo animar entradas y salidas con View Transitions"},"content":{"rendered":"<p>React 19.3 sali\u00f3 el 9 de septiembre y trae las <strong>View Transitions<\/strong> como componente estable. Si alguna vez escribiste CSS <code>transition<\/code> a mano para que un elemento apareciera y desapareciera sin brusquedad, esto te interesa: ahora le pides al navegador la animaci\u00f3n y React decide sola cu\u00e1ndo entra, sale, se mueve o se actualiza cada cosa. En esta gu\u00eda vas a construir dos casos reales con View Transitions en React 19.3: una lista que anima sus elementos y una galer\u00eda que cambia de diapositiva seg\u00fan la direcci\u00f3n.<\/p>\n<h2>Qu\u00e9 necesitas antes de empezar<\/h2>\n<p>Antes de escribir c\u00f3digo, actualiza a la versi\u00f3n nueva y verifica que tu navegador soporta la API sobre la que todo esto se apoya:<\/p>\n<pre><code class=\"language-bash\">npm install react@19.3 react-dom@19.3<\/code><\/pre>\n<p>Las View Transitions de React usan la View Transition API del navegador, as\u00ed que necesitas un navegador que la implemente \u2014 Chrome la tiene completa; consulta el soporte actual en <a href=\"https:\/\/caniuse.com\/view-transition-api\" target=\"_blank\" rel=\"noopener\">caniuse.com<\/a> antes de darla por hecha en producci\u00f3n. Es una API del DOM: en React Native todav\u00eda no funciona, est\u00e1 en camino.<\/p>\n<p>Y la regla que m\u00e1s confusiones evita: <strong>una animaci\u00f3n solo ocurre si el cambio de estado est\u00e1 marcado como Transition<\/strong>. Sin <code>startTransition<\/code>, no hay animaci\u00f3n. Esto es lo primero que revisa todo el mundo cuando \u00abno funciona\u00bb.<\/p>\n<h2>C\u00f3mo funcionan las View Transitions en React 19.3<\/h2>\n<p>Envuelves lo que quieres animar en un componente <code>ViewTransition<\/code> y React clasifica sola lo que pas\u00f3:<\/p>\n<pre><code class=\"language-js\">import { ViewTransition, useState, startTransition } from 'react';\n\nfunction App() {\n  const [showItem, setShowItem] = useState(false);\n\n  return (\n    &lt;&gt;\n      &lt;button onClick={() =&gt; {\n        startTransition(() =&gt; {\n          setShowItem((prev) =&gt; !prev);\n        });\n      }}&gt;\n        Mostrar u ocultar\n      &lt;\/button&gt;\n\n      {showItem &amp;&amp; (\n        &lt;ViewTransition&gt;\n          &lt;MiTarjeta \/&gt;\n        &lt;\/ViewTransition&gt;\n      )}\n    &lt;\/&gt;\n  );\n}<\/code><\/pre>\n<p>React elige el tipo de animaci\u00f3n seg\u00fan lo que cambi\u00f3 en el \u00e1rbol:<\/p>\n<ul>\n<li><strong>enter<\/strong>: el <code>ViewTransition<\/code> se a\u00f1adi\u00f3 al \u00e1rbol \u2014 el contenido aparece.<\/li>\n<li><strong>exit<\/strong>: se quit\u00f3 del \u00e1rbol \u2014 el contenido desaparece.<\/li>\n<li><strong>update<\/strong>: el contenido o sus estilos cambiaron sin entrar ni salir.<\/li>\n<li><strong>share<\/strong>: un <code>ViewTransition<\/code> con nombre se quit\u00f3 de un lugar y apareci\u00f3 en otro \u2014 el elemento \u00abviaja\u00bb.<\/li>\n<\/ul>\n<p>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 <code>onEnter<\/code>, <code>onExit<\/code>, <code>onUpdate<\/code> y <code>onShare<\/code> con la Web Animations API.<\/p>\n<h2>Tu primera animaci\u00f3n de lista<\/h2>\n<p>Vamos al caso completo: una lista donde cada elemento aparece con fade y el bot\u00f3n dispara todo dentro de un <code>startTransition<\/code>.<\/p>\n<pre><code class=\"language-js\">import { ViewTransition, useState, startTransition } from 'react';\n\nexport function ListaDeTareas() {\n  const [tareas, setTareas] = useState([\n    { id: 1, texto: 'Revisar el pull request' },\n    { id: 2, texto: 'Subir el deploy del viernes' },\n  ]);\n  const [nueva, setNueva] = useState('');\n\n  function agregar() {\n    startTransition(() =&gt; {\n      setTareas((prev) =&gt; [\n        ...prev,\n        { id: Date.now(), texto: nueva },\n      ]);\n    });\n    setNueva('');\n  }\n\n  return (\n    &lt;div&gt;\n      &lt;input\n        value={nueva}\n        onChange={(e) =&gt; setNueva(e.target.value)}\n        placeholder=\"Nueva tarea\"\n      \/&gt;\n      &lt;button onClick={agregar}&gt;Agregar&lt;\/button&gt;\n\n      &lt;ul&gt;\n        {tareas.map((tarea) =&gt; (\n          &lt;ViewTransition key={tarea.id}&gt;\n            &lt;li&gt;{tarea.texto}&lt;\/li&gt;\n          &lt;\/ViewTransition&gt;\n        ))}\n      &lt;\/ul&gt;\n    &lt;\/div&gt;\n  );\n}<\/code><\/pre>\n<p>C\u00f3mo comprobar que funcion\u00f3: escribe algo, dale agregar y observa que el elemento nuevo aparece con un fade en vez de saltar. Si aparece instant\u00e1neo, revisa que el <code>setTareas<\/code> est\u00e9 <strong>dentro<\/strong> de <code>startTransition<\/code> \u2014 es el error m\u00e1s com\u00fan.<\/p>\n<h2>Animaciones seg\u00fan la causa con addTransitionType<\/h2>\n<p>El cross-fade est\u00e1 bien para empezar, pero el caso interesante es cuando la misma acci\u00f3n se anima distinto seg\u00fan por qu\u00e9 pas\u00f3. Una galer\u00eda que avanza deber\u00eda deslizar hacia la izquierda; la misma galer\u00eda retrocediendo deber\u00eda deslizar hacia la derecha. Eso se resuelve con <code>addTransitionType<\/code>:<\/p>\n<pre><code class=\"language-js\">import {\n  ViewTransition,\n  useState,\n  startTransition,\n  addTransitionType,\n} from 'react';\n\nfunction siguiente() {\n  startTransition(() =&gt; {\n    addTransitionType('next');\n    setIndice((i) =&gt; i + 1);\n  });\n}\n\nfunction anterior() {\n  startTransition(() =&gt; {\n    addTransitionType('previous');\n    setIndice((i) =&gt; i - 1);\n  });\n}\n\nexport function Galeria({ fotos }) {\n  const [indice, setIndice] = useState(0);\n  const foto = fotos[indice];\n\n  return (\n    &lt;&gt;\n      &lt;button onClick={anterior}&gt;Anterior&lt;\/button&gt;\n      &lt;button onClick={siguiente}&gt;Siguiente&lt;\/button&gt;\n\n      &lt;ViewTransition\n        key={foto.id}\n        enter={{\n          next: 'desde-derecha',\n          previous: 'desde-izquierda',\n        }}\n        exit={{\n          next: 'hacia-izquierda',\n          previous: 'hacia-derecha',\n        }}\n      &gt;\n        &lt;img src={foto.url} alt={foto.alt} \/&gt;\n      &lt;\/ViewTransition&gt;\n    &lt;\/&gt;\n  );\n}<\/code><\/pre>\n<p>Y las clases de animaci\u00f3n viven en tu CSS, donde el tipo de transici\u00f3n queda disponible como pseudo-clase:<\/p>\n<pre><code class=\"language-css\">::view-transition-new(desde-derecha) {\n  animation: entrarDesdeDerecha 250ms ease-out;\n}\n\n::view-transition-old(hacia-izquierda) {\n  animation: salirHaciaIzquierda 250ms ease-in;\n}<\/code><\/pre>\n<p>Los tipos tambi\u00e9n llegan al navegador como view transition types, as\u00ed que puedes usar <code>:active-view-transition-type(...)<\/code> en CSS para apuntar a una transici\u00f3n concreta.<\/p>\n<h2>ViewTransition junto a Suspense<\/h2>\n<p>El patr\u00f3n que recomienda el equipo de React para datos as\u00edncronos: el fallback aparece <strong>sin animaci\u00f3n<\/strong> (debe ser inmediato), y solo la actualizaci\u00f3n de fallback a contenido se anima:<\/p>\n<pre><code class=\"language-js\">&lt;ViewTransition update=\"auto\" default=\"none\"&gt;\n  &lt;Suspense fallback={&lt;Cargando \/&gt;}&gt;\n    &lt;Perfiles \/&gt;\n  &lt;\/Suspense&gt;\n&lt;\/ViewTransition&gt;<\/code><\/pre>\n<p>Ese <code>update=\"auto\" default=\"none\"<\/code> dice expl\u00edcito lo que quieres: la entrada del fallback no se anima, la actualizaci\u00f3n s\u00ed. Sin este par\u00e1metro corres el riesgo de que el fallback aparezca con fade y la interfaz se sienta lenta cuando en realidad est\u00e1 cargando r\u00e1pido.<\/p>\n<h2>Lo dem\u00e1s que llega en 19.3<\/h2>\n<p>Tres adiciones m\u00e1s que valen la pena conocer, sin tutorial completo aqu\u00ed:<\/p>\n<p><strong>Fragment Refs.<\/strong> Ahora puedes pasar un <code>ref<\/code> a un <code>Fragment<\/code> y operar sobre los hijos como grupo, sin un elemento envolvente que ensucie el DOM:<\/p>\n<pre><code class=\"language-js\">import { Fragment, useRef, useEffect } from 'react';\n\nfunction ListaDeEncabezados({ posts }) {\n  const ref = useRef(null);\n\n  useEffect(() =&gt; {\n    ref.current.focus();\n  }, []);\n\n  return (\n    &lt;Fragment ref={ref}&gt;\n      {posts.map((post) =&gt; (\n        &lt;h2 key={post.id}&gt;{post.titulo}&lt;\/h2&gt;\n      ))}\n    &lt;\/Fragment&gt;\n  );\n}<\/code><\/pre>\n<p><strong>El API browser().<\/strong> El reemplazo de primera clase para el cl\u00e1sico <code>typeof window !== 'undefined'<\/code>: el componente suspende en el servidor y renderiza normal en el cliente:<\/p>\n<pre><code class=\"language-js\">import { use } from 'react';\nimport { browser } from 'react-dom';\n\nfunction ZonaHoraria({ valorPorDefecto }) {\n  if (valorPorDefecto) {\n    return &lt;p&gt;{valorPorDefecto}&lt;\/p&gt;;\n  }\n\n  use(browser());\n  const zona = new Intl.DateTimeFormat().resolvedOptions().timeZone;\n  return &lt;p&gt;{zona}&lt;\/p&gt;;\n}<\/code><\/pre>\n<p>A diferencia de los hooks, <code>use<\/code> s\u00ed puede llamarse en un condicional.<\/p>\n<p><strong>Contexto en Server Components.<\/strong> Un Server Component puede renderizar directamente un Context importado de un m\u00f3dulo <code>'use client'<\/code>, sin el componente Provider envolviendo. Los Server Components siguen sin poder crear Context, solo renderizarlo.<\/p>\n<h2>Errores frecuentes<\/h2>\n<ul>\n<li><strong>No se anima nada.<\/strong> El cambio de estado no est\u00e1 dentro de <code>startTransition<\/code>. Es la causa n\u00famero uno y la primera que revisa.<\/li>\n<li><strong>Funciona en mi navegador pero no en todos.<\/strong> La View Transition API es del navegador; sin soporte, no hay animaci\u00f3n \u2014 el contenido aparece y desaparece como siempre, no rompe nada, pero tampoco anima.<\/li>\n<li><strong>Mis efectos se ejecutan dos veces en hidrataci\u00f3n.<\/strong> No es un bug tuyo: en 19.3 el Strict Mode durante la hidrataci\u00f3n ahora invoca los efectos dos veces, igualando lo que ya pasaba en render del cliente. Revisa que tus efectos sean idempotentes.<\/li>\n<li><strong>Las transiciones lentas bloqueaban a las dem\u00e1s.<\/strong> Ya no: en 19.3 las transiciones renderizan de forma independiente y una lenta no detiene a las dem\u00e1s.<\/li>\n<\/ul>\n<p>Fuente original: el <a href=\"https:\/\/react.dev\/blog\/2026\/09\/09\/react-19-3\" target=\"_blank\" rel=\"noopener\">anuncio de React 19.3 en react.dev<\/a>, con la referencia completa del componente en la <a href=\"https:\/\/react.dev\/reference\/react\/ViewTransition\" target=\"_blank\" rel=\"noopener\">documentaci\u00f3n de ViewTransition<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>View Transitions ya es estable en React 19.3. Esta gu\u00eda muestra c\u00f3mo animar la interfaz con ViewTransition y addTransitionType, con ejemplos completos y los errores t\u00edpicos.<\/p>\n","protected":false},"author":2,"featured_media":35,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[10,9],"class_list":["post-19","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-frontend","tag-javascript","tag-react"],"_links":{"self":[{"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/posts\/19","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/comments?post=19"}],"version-history":[{"count":3,"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/posts\/19\/revisions"}],"predecessor-version":[{"id":28,"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/posts\/19\/revisions\/28"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/media\/35"}],"wp:attachment":[{"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/media?parent=19"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/categories?post=19"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/desarrollosms.com\/index.php\/wp-json\/wp\/v2\/tags?post=19"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}