React 19.3 más allá de los titulares: submitter, propiedades Fullscreen y tamaño del paquete
Un recorrido práctico por los cambios menores de React 19.3: la propiedad submitter, los atributos del evento fullscreen, las transiciones paralelas, onReset, correcciones importantes de errores y el aumento real del tamaño.
Las notas de lanzamiento suelen destacar un par de características principales, y React 19.3 no es la excepción. Debajo de ellas se encuentra una lista de “otros cambios notables” además de una extensa sección con correcciones de errores, la mayoría de las cuales son una sola línea que es fácil pasar por alto. Sin embargo, varias de esas líneas modifican la forma en que se escriben los componentes hoy en día, explican errores con los que quizás ya hayas lidiado o afectan lo que se envía a los usuarios. Esta guía las analiza aproximadamente en orden de cuán relevantes podrían ser para ti, y finaliza con una respuesta meditada a la pregunta que todos se hacen después de una actualización: ¿cuánto más grande se ha vuelto el paquete?
Saber qué botón envió un formulario
Un formulario con dos botones de envío, por ejemplo Guardar y Guardar y cerrar, necesita saber cuál de ellos presionó el usuario. Los navegadores han respondido a esta pregunta desde hace tiempo: el evento nativo SubmitEvent expone una propiedad submitter que apunta al botón que activó el envío. Sin embargo, el evento sintético de React no la transmitía. Las soluciones habituales consistían en leer e.nativeEvent.submitter o, de forma menos elegante, registrar en el estado el último botón pulsado con la esperanza de que no hubiera envíos por teclado que se escaparan. React 19.3 agrega submitter al evento de envío sintético (#35590).
La forma más sencilla de ver la diferencia es probar un formulario con ambas versiones. La demostración comienza con sus importaciones; la importación de espacio de nombres está allí únicamente para que el componente pueda imprimir React.version.
import { useState } from 'react';
import * as React from 'react';
El componente impide la navegación por defecto y luego escribe la versión de React junto con el valor de e.submitter en el estado. Cada botón tiene su propio value, que es exactamente lo en lo que se tomaría una decisión condicional en el código real.
export default function App() {
const [msg, setMsg] = useState('press either button');
return (
<form onSubmit={e => {
e.preventDefault();
setMsg(`${React.version}: e.submitter → ${e.submitter ? e.submitter.value : String(e.submitter)}`);
}}>
<button value="save">Save</button>{' '}
<button value="save-close">Save & close</button>
<p className="msg">{msg}</p>
</form>
);
}
Una única regla estila la línea de salida con una fuente monoespaciada.
.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }
El mismo formulario en 19.3
La versión 19.3 de la demostración es idéntica, y ese es el objetivo: solo cambia la versión de React. Las importaciones son las mismas.
import { useState } from 'react';
import * as React from 'react';
Lo mismo ocurre con el componente.
export default function App() {
const [msg, setMsg] = useState('press either button');
return (
<form onSubmit={e => {
e.preventDefault();
setMsg(`${React.version}: e.submitter → ${e.submitter ? e.submitter.value : String(e.submitter)}`);
}}>
<button value="save">Save</button>{' '}
<button value="save-close">Save & close</button>
<p className="msg">{msg}</p>
</form>
);
}
Y lo mismo con la hoja de estilos.
.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }
En la versión 19.2 el mensaje muestra undefined, ya que el evento sintético no cuenta con dicho campo. En la versión 19.3 se muestra save o save-close, dependiendo del botón. Tenga en cuenta qué ruta de código se ve afectada. Si envía los datos a través de las acciones del formulario en lugar de un manejador onSubmit, React ya estaba leyendo el elemento que envía los datos al crear FormData (#29028); por lo tanto, la pareja name y value del botón presionado ya estaba presente en los datos de la acción. La nueva propiedad es importante para los manejadores de eventos.
Nota práctica: el value del elemento que envía los datos solo sirve de ayuda si realmente establece value (y generalmente también name) en cada botón. Un botón sin estos valores genera una cadena vacía, lo cual se puede confundir fácilmente con una función faltante.
Eventos de pantalla completa como propiedades de primera clase
La API Fullscreen envía los eventos fullscreenchange y fullscreenerror al elemento afectado. React no reconoció esos nombres de eventos, por lo que componentes como los reproductores de video tuvieron que adjuntar escuchadores manualmente con addEventListener dentro de un efecto y recordar eliminarlos al limpiar. React 19.3 añade onFullscreenChange y onFullscreenError como propiedades normales (#34621).
La demostración necesita un ref para el elemento que pasa a pantalla completa y un estado para el registro de eventos.
import { useRef, useState } from 'react';
Obsérvese que ambos manejadores se encuentran directamente sobre el div. El manejador de cambios verifica document.fullscreenElement para determinar si el elemento acaba de entrar o salir en modo pantalla completa, ya que el evento se dispara en ambas direcciones. El botón Ir a pantalla completa también agrega un .catch a la promesa devuelta por requestFullscreen(), ya que el rechazo se reporta tanto como una promesa rechazada como un evento de error.
export default function App() {
const box = useRef(null);
const [log, setLog] = useState([]);
const add = line => setLog(l => [...l, line]); return (
<div
ref={box}
className="stage"
onFullscreenChange={() => add(document.fullscreenElement ? 'entered fullscreen' : 'left fullscreen')}
onFullscreenError={e => add('fullscreen error: ' + e.type)}
>
<button onClick={() => box.current.requestFullscreen().catch(err => add('rejected: ' + err.message))}>
Go fullscreen
</button>{' '}
<button onClick={() => document.fullscreenElement && document.exitFullscreen()}>Exit</button>
<pre className="log">{log.join('\n') || 'no events yet'}</pre>
</div>
);
}
La hoja de estilo utiliza la pseudo-clase :fullscreen para centrar el contenido mientras el escenario llena toda la pantalla, y light-dark() para elegir un fondo que se adapte al esquema de colores.
.stage { padding: 12px; border-radius: 10px; background: light-dark(#f1f5f9, #262a33); }
.stage:fullscreen { display: grid; place-content: center; }
.log { margin: 10px 0 0; font-size: 14px; }
onFullscreenError se dispara cuando el navegador rechaza la solicitud. Las dos causas más comunes son llamar a requestFullscreen() fuera de un gesto del usuario y ejecutarlo dentro de un <iframe> que carece de la opción allow="fullscreen". Si incrustas un reproductor en una página de terceros y el modo pantalla completa no funciona, lo primero que debes verificar es esa falta de permiso en el iframe; la nueva propiedad te proporciona un lugar adecuado para registrarlo.
Las transiciones ya no esperan unas a otras
Según la entrada del changelog, cada transición ahora se renderiza por separado en lugar de combinarse con las demás en una sola renderización, de modo que una transición lenta no pueda bloquear a las restantes. Este cambio (#37290) habilita la bandera enableParallelTransitions.
Antes de la versión 19.3, si llegaba una segunda actualización de startTransition mientras React aún estaba procesando la primera, ambas se fusionaban. El proceso de renderizado en curso se reiniciaba con ambas actualizaciones aplicadas, y ninguna podía completarse hasta que finalizara ese renderizado combinado. Por lo tanto, una transición rápida y sencilla podía quedar retenida por otra más lenta con la que no tenía relación alguna. Ahora, cada transición es una unidad de trabajo independiente. Si deseas entender cómo React hace un seguimiento interno de esas unidades, nuestro artículo sobre cómo React codifica la prioridad de las actualizaciones explica el modelo subyacente.
La forma en que se escriben las transiciones no cambia, y siguen aplicándose las garantías de ordenamiento de React: una actualización posterior nunca se aplicará junto con un estado anterior. Sin embargo, hay una suposición que ya no es válida: las actualizaciones realizadas a través de dos llamadas diferentes a startTransition ahora pueden procesarse en frames distintos. Si varias actualizaciones deben aparecer juntas, colóquelas en una sola llamada a startTransition.
No espere ver esto en un ejemplo sencillo. Un intento por reproducirlo con un árbol lento de 300 componentes y una actualización rápida y trivial programada 50 ms después mostró que la actualización rápida se aplicaba después de la lenta tanto en 19.2 como en 19.3. La mejora se da a nivel del programador de tareas y se observa en aplicaciones grandes bajo carga real, no en demos pequeñas.
Formularios: onReset después de una acción, y nueva redacción para los errores
Cuando se completa la acción de un formulario, React reinicia automáticamente el formulario. Hasta ahora realizaba ese reinicio con su sistema de eventos desactivado, lo que significaba que el evento reset no se procesaba. El manejador onReset, diseñado para borrar el estado controlado, nunca se ejecutaba, y los campos de entrada controlados seguían mostrando sus valores anteriores (#35176, que soluciona el problema #33630). En la versión 19.3, onReset también se dispara durante el reinicio automático. Si usted solucionó el error escuchando personalmente el evento nativo reset, puede volver a utilizar la propiedad correspondiente y eliminar el efecto asociado.
Un cambio estético relacionado: los mensajes de error de useActionState ahora se refieren a “estado de acción” en lugar de “estado del formulario” (#35790). El comportamiento no cambia, pero las búsquedas en los registros, las reglas de alerta o las pruebas que utilizan la terminología anterior necesitan actualizarse.
Cambios en tiempo de desarrollo: Modo estricto, use() y actualización rápida
Tres cambios afectan únicamente a las compilaciones de desarrollo, pero cada uno puede revelar algo sorprendente después de una actualización.
- Modo estricto durante la hidratación. Ahora los efectos se invocan dos veces al hidratarse en Modo estricto (#35961), lo que coincide con el comportamiento que han tenido las raíces renderizadas por el cliente desde React 18. Si una aplicación renderizada por el servidor parecía segura en Modo estricto solo porque la hidratación omitía la segunda invocación, esto lo hará evidente. La solución es la habitual: hacer que los efectos sean idempotentes y proporcionarles una limpieza adecuada.
use(). Está permitido llamar a use de forma condicional. El caso problemático ocurre cuando un componente se suspende en use() y, al intentarlo de nuevo, toma un camino que nunca llama a use() en absoluto. Como resultado, el límite de Suspense se resuelve por la razón incorrecta. Ahora React emite una advertencia de desarrollo que explica que una biblioteca se suspendió mediante use() en un render anterior, omitió llamar a use() una vez finalizado y que esto constituye un uso incorrecto del hook. La redacción sugiere que el culpable suele ser una biblioteca de datos y no tu propio componente (#37104, #37203, #37491).lazy(), una función de comparación de memo() modificada, y una edición que convierte un componente en otro tipo de componente (#36965, #36964, #36963, #36950). Si esas ediciones obligaban a veces a un recargue completo de la página, estas eran las causas.Cambios menores en el DOM
- Las actualizaciones de
resizeahora se agrupan hasta el siguiente frame (#35117), de la misma manera que ya ocurre con los eventos de desplazamiento. El estado establecido en un listener deresizeya no se envía prematuramente solo porque llega un clic en medio de un frame. maskTypeahora se reconoce como una propiedad de SVG (#35921).- Los recursos de módulo ahora pueden incluir una indicación de prioridad: pase
fetchPriorityaReactDOM.preloadModuleoReactDOM.preinitModuley React lo renderizará como un atributofetchpriority(#36835). Esto permite a un plugin de compilación RSC marcar los fragmentos de componentes del cliente como de baja prioridad. Las funciones no relacionadas con módulospreloadypreinitya lo soportaban. credentiallessse trata como un atributo booleano en<iframe>(#36148), y React sugiere la forma correcta de escribirlo si se utilizacredentialLess(#36322).- React ya no vuelve a asignar
innerHTMLcuando este no ha cambiado (#36949). Volver a renderizar con undangerouslySetInnerHTMLidéntico ya no hace que el navegador analice nuevamente el marcado, lo cual también preserva cualquier estado del DOM que contenga. defaultValueen los campos de tipo"number"ahora se comporta de la misma manera que en otros tipos de campos (#36980).- El evento
togglecopia la propiedadsourcepara los popovers (#37389).
Renderizado en el servidor y Componentes del servidor
- Los errores generados en el servidor ahora llegan al cliente junto con su
cause(#35810), y unAggregateErrormantiene su arrayerrors(#36156). Los límites de errores y el código de registro que analizan la cadena de errores ven la misma estructura en ambos lados. - Los Componentes del servidor ahora pueden renderizar
<Activity>, ya que Flight lo comprende (#34697). react-dom/serverya no se bloquea en Deno (#35235), y el punto de entrada de Bun ahora incluye todas las API de Node (#34193).
Correcciones de errores cuyos síntomas podrían haber visto
La lista completa es larga. Estas son las entradas que con mayor probabilidad corresponden a un error que ya has investigado.
useDeferredValuepodría quedar atascado en un valor obsoleto (#36134). Un valor diferido que ocasionalmente nunca se actualizaba era un error de React, no un error en tu código.- Los cambios de contexto no siempre llegaban a los mecanismos de respaldo de Suspense (#36160) ni a un subárbol detrás de un límite que seguía en estado suspendido (#35839).
useEffectEventdentro de los componentesforwardRefymemoleía valores obsoletos (#34831).- Mientras un árbol
<Activity>estaba oculto,useSyncExternalStorepasaba por alto las mutaciones del almacén (#36947). Además, un árbol oculto deja de elevar su<title>(#34983), mantiene sus errores fuera de la interfaz visible (#35074) y ya no muestra lo que renderiza en los portales (#35091).
nonce (#37030).<ViewTransition>: una en Mobile Safari (#35337) y otra al usarlo junto con SuspenseList (#35520). Esto es importante si probó las versiones experimentales.decodeReplyFromBusboy descartaba los elementos FormData (#36468), las cadenas asíncronas profundas podían agotar la pila de ejecución (#35612), y la información de depuración podía crecer exponencialmente hasta provocar un RangeError (#37481).El conjunto completo de solicitudes de integración está disponible en el CHANGELOG de React.
El costo de la actualización en bytes
Poco después del lanzamiento, un comentario en la discusión de r/reactjs señaló un aumento en el tamaño de react-dom-client.production.js de 523,5 kB a 610,5 kB y preguntó por la razón. Al comparar los paquetes 19.2.0 y 19.3.0 publicados en unpkg, se obtienen las siguientes cifras:
cjs/react-dom-client.production.js, en formato raw: 536,016 B en la versión 19.2.0 frente a 625,168 B en la versión 19.3.0, lo que representa un aumento de 89 kB (16,6 %).- El mismo archivo con compresión gzip al nivel 9: 94,7 kB frente a 110,5 kB, es decir, un aumento de 15,7 kB.
cjs/react-dom-server.node.production.js, en formato raw: 277,046 B frente a 300,544 B, lo que supone un aumento de 23,5 kB.cjs/react.production.js, en formato raw: 17,217 B frente a 18,040 B, con un aumento de 0,8 kB.
Las cifras en los comentarios de Reddit tienen el mismo tamaño expresado en KiB. La causa es sencilla: las transiciones de vista, los Fragment Refs, browser() y la ruta de código de Trusted Types se utilizaban detrás de flags de funcionalidad que los eliminaron de la versión estable. En la versión 19.3 ya vienen incluidos. A través de la red, la diferencia es aproximadamente de 16 kB comprimidos, lo cual es comparable a un conjunto de íconos de tamaño medio.
Ese es un costo real que merece ser tenido en cuenta en el presupuesto de rendimiento, pero no representa una regresión en el comportamiento de React. Si los bytes adicionales son importantes para tu aplicación, las estrategias más efectivas suelen ser el split de código, reducir tus propias dependencias y posponer la carga de componentes cliente no críticos, en lugar de retrasar la actualización.
Puntos clave
- Lea
e.submitterdirectamente en los manejadoresonSubmit; las acciones de formulario ya reciben elnameyvaluedel botón. - Sustituya los listeners manuales de
fullscreenchangeporonFullscreenChangeyonFullscreenError, y también maneje la promesa rechazada porrequestFullscreen(). - Las llamadas separadas a
startTransitionahora pueden ejecutarse en diferentes frames, por lo que agrupe las actualizaciones que deben aparecer juntas. onResetfinalmente se ejecuta después del reinicio automático de una acción, y el Modo Estricto ahora invoca dos veces los efectos durante la hidratación, así que verifique nuevamente la limpieza de los efectos.- Varios errores crónicos, incluyendo un
useDeferredValueatascado y valores obsoletos deuseEffectEvent, se han corregido; revise si alguna solución temporal en su código puede eliminarse.
react-dom en el cliente; las funciones anteriormente marcadas como opcionales ahora están incluidas en las versiones estables.