useTransition
useTransition
es un Hook de React que te permite actualizar el estado sin bloquear la UI.
const [isPending, startTransition] = useTransition()
- Referencia
- Uso
- Marcar una actualización de estado como transición no bloqueante
- Actualización del componente principal en una transición
- Visualización de un estado visual pendiente durante la transición
- Evitar indicadores de carga no deseados
- Construir un enrutador preparado para Suspense
- Displaying an error to users with a error boundary
- Solución de problemas
Referencia
useTransition()
Llama a useTransition
en el nivel superior de tu componente para marcar algunas actualizaciones de estado como transiciones.
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
Parámetros
useTransition
no recibe ningun parámetro.
Devuelve
useTransition
devuelve un array con exactamente dos elementos:
isPending
que indica si hay una transición pendiente.startTransition
function que permite marcar una actualización de estado como una transición.
Función startTransition
La función startTransition
devuelta por useTransition
permite marcar una actualización de estado como una transición.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
Parámetros
scope
: Una función que actualiza algún estado llamando a una o varias funcionesset
. React inmediatamente llama ascope
sin parámetros y marca todas las actualizaciones de estado programadas sincrónicamente durante la llamada a la funciónscope
como transiciones. Serán sin bloqueo y no mostrarán indicadores de carga no deseados.
Devuelve
startTransition
no devuelve nada.
Advertencias
-
useTransition
es un Hook, por lo que sólo puede ser llamado dentro de componentes o Hooks personalizados. Si necesitas iniciar una transición en otro lugar (por ejemplo, desde una biblioteca de datos), llama a la función independientestartTransition
en su lugar. -
Puedes envolver una actualización en una transición sólo si tienes acceso a la función
set
de ese estado. Si deseas iniciar una transición en respuesta a alguna prop o algún valor de un Hook personalizado, pruebauseDeferredValue
en su lugar. -
La función que pases a
startTransition
debe ser síncrona. React ejecuta inmediatamente esta función, marcando como transiciones todas las actualizaciones de estado que se produzcan mientras se ejecuta. Si intentas realizar más actualizaciones de estado más tarde (por ejemplo, en un tiempo de espera), no se marcarán como transiciones. -
Una actualización de estado marcada como transición será interrumpida por otras actualizaciones de estado. Por ejemplo, si actualizas un componente gráfico dentro de una transición, pero luego empiezas a escribir en un input mientras el gráfico está en medio de un rerenderizado, React reiniciará el trabajo de renderizado en el componente gráfico después de gestionar la actualización del input.
-
Las actualizaciones de transición no pueden utilizarse para controlar las entradas de texto.
-
Si hay varias transiciones en curso, React las agrupa. Se trata de una limitación que probablemente se eliminará en una versión futura.
Uso
Marcar una actualización de estado como transición no bloqueante
Llama a useTransition
en el nivel superior de tu componente para marcar las actualizaciones de estado como transiciones no bloqueantes.
import { useState, useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
useTransition
devuelve un array con exactamente dos elementos:
isPending
flag que te indica si hay una transición pendiente.startTransition
function que permite marcar una actualización de estado como una transición.
A continuación, puedes marcar una actualización de estado como una transición de esta manera:
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
Las transiciones permiten mantener la capacidad de respuesta de las actualizaciones de la interfaz de usuario incluso en dispositivos lentos.
Con una transición, la interfaz de usuario mantiene su capacidad de respuesta en medio de una nueva renderización. Por ejemplo, si el usuario hace clic en una pestaña pero luego cambia de opinión y hace clic en otra, puede hacerlo sin esperar a que termine la primera renderización.
Ejemplo 1 de 2: Actualizar la pestaña actual en una transición
En este ejemplo, la pestaña «Posts» está artificialmente ralentizada para que tarde al menos un segundo en renderizarse.
Haz clic en «Mensajes» y luego inmediatamente en «Contacto». Observa que esto interrumpe la renderización lenta de «Posts». La pestaña «Contact» se muestra inmediatamente. Debido a que esta actualización de estado está marcada como una transición, una renderización lenta no congela la interfaz de usuario.
import { useState, useTransition } from 'react'; import TabButton from './TabButton.js'; import AboutTab from './AboutTab.js'; import PostsTab from './PostsTab.js'; import ContactTab from './ContactTab.js'; export default function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState('about'); function selectTab(nextTab) { startTransition(() => { setTab(nextTab); }); } return ( <> <TabButton isActive={tab === 'about'} onClick={() => selectTab('about')} > Acerca de </TabButton> <TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')} > Posts (slow) </TabButton> <TabButton isActive={tab === 'contact'} onClick={() => selectTab('contact')} > Contact </TabButton> <hr /> {tab === 'about' && <AboutTab />} {tab === 'posts' && <PostsTab />} {tab === 'contact' && <ContactTab />} </> ); }
Actualización del componente principal en una transición
También puedes actualizar el estado de un componente padre desde la llamada «useTransition». Por ejemplo, este componente TabButton
envuelve su lógica onClick
en una transición:
export default function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
onClick();
});
}}>
{children}
</button>
);
}
Debido a que el componente padre actualiza su estado dentro del controlador de evento onClick
, esa actualización de estado se marca como una transición. Esta es la razón por la que, como en el ejemplo anterior, puedes hacer clic en «Posts» y luego inmediatamente hacer clic en «Contacto». La actualización de la pestaña seleccionada se marca como una transición, por lo que no bloquea las interacciones del usuario.
import { useTransition } from 'react'; export default function TabButton({ children, isActive, onClick }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } return ( <button onClick={() => { startTransition(() => { onClick(); }); }}> {children} </button> ); }
Visualización de un estado visual pendiente durante la transición
Puedes utilizar el valor booleano isPending
devuelto por useTransition
para indicar al usuario que una transición está en curso. Por ejemplo, el botón de la pestaña puede tener un estado visual especial «pendiente»:
function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...
Fíjate en que hacer clic en «Posts» ahora es más sensible porque el botón de la pestaña se actualiza inmediatamente:
import { useTransition } from 'react'; export default function TabButton({ children, isActive, onClick }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={() => { startTransition(() => { onClick(); }); }}> {children} </button> ); }
Evitar indicadores de carga no deseados
En este ejemplo, el componente PostsTab
obtiene algunos datos utilizando una fuente de datos preparada par usarse con Suspense. Al hacer clic en la pestaña «Posts», el componente PostsTab
se suspende, haciendo que aparezca el fallback de carga más cercano:
import { Suspense, useState } from 'react'; import TabButton from './TabButton.js'; import AboutTab from './AboutTab.js'; import PostsTab from './PostsTab.js'; import ContactTab from './ContactTab.js'; export default function TabContainer() { const [tab, setTab] = useState('about'); return ( <Suspense fallback={<h1>🌀 Loading...</h1>}> <TabButton isActive={tab === 'about'} onClick={() => setTab('about')} > Acerca de </TabButton> <TabButton isActive={tab === 'posts'} onClick={() => setTab('posts')} > Posts </TabButton> <TabButton isActive={tab === 'contact'} onClick={() => setTab('contact')} > Contact </TabButton> <hr /> {tab === 'about' && <AboutTab />} {tab === 'posts' && <PostsTab />} {tab === 'contact' && <ContactTab />} </Suspense> ); }
Ocultar todo el contenedor de pestañas para mostrar un indicador de carga conduce a una experiencia de usuario discordante. Si añades useTransition
a TabButton
, puedes indicar que se muestre el estado pendiente en el botón de la pestaña.
Observa que al hacer clic en «Entradas» ya no se sustituye todo el contenedor de la pestaña por un spinner:
import { useTransition } from 'react'; export default function TabButton({ children, isActive, onClick }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={() => { startTransition(() => { onClick(); }); }}> {children} </button> ); }
Más información sobre el uso de transiciones con Suspense.
Construir un enrutador preparado para Suspense
Si estás construyendo un framework de React o un enrutador, te recomendamos marcar las navegaciones de página como transiciones.
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
Esto se recomienda por dos razones:
- Las transiciones son interrumpibles, que permite al usuario hacer clic sin esperar a que se complete la nueva renderización.
- Las transiciones evitan indicadores de carga no deseados, que permite al usuario evitar saltos bruscos en la navegación.
He aquí un pequeño ejemplo de enrutador simplificado que utiliza transiciones para las navegaciones.
import { Suspense, useState, useTransition } from 'react'; import IndexPage from './IndexPage.js'; import ArtistPage from './ArtistPage.js'; import Layout from './Layout.js'; export default function App() { return ( <Suspense fallback={<BigSpinner />}> <Router /> </Suspense> ); } function Router() { const [page, setPage] = useState('/'); const [isPending, startTransition] = useTransition(); function navigate(url) { startTransition(() => { setPage(url); }); } let content; if (page === '/') { content = ( <IndexPage navigate={navigate} /> ); } else if (page === '/the-beatles') { content = ( <ArtistPage artist={{ id: 'the-beatles', name: 'The Beatles', }} /> ); } return ( <Layout isPending={isPending}> {content} </Layout> ); } function BigSpinner() { return <h2>🌀 Loading...</h2>; }
Displaying an error to users with a error boundary
Si una función que se pasa a startTransition
lanza un error, puedes mostrar un error a tu usuario con una barrera de error. Para usar una barrera de error, envuelve el componente en el que estás llamando a useTransition
en una barrera de error. Una vez que la función que se pasa a startTransition
lanza un error se mostrará el fallback de la barrera de error.
import { useTransition } from "react"; import { ErrorBoundary } from "react-error-boundary"; export function AddCommentContainer() { return ( <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}> <AddCommentButton /> </ErrorBoundary> ); } function addComment(comment) { // For demonstration purposes to show Error Boundary if(comment == null){ throw Error('Example error') } } function AddCommentButton() { const [pending, startTransition] = useTransition(); return ( <button disabled={pending} onClick={() => { startTransition(() => { // Intentionally not passing a comment // so error gets thrown addComment(); }); }}> Add comment </button> ); }
Solución de problemas
No funciona la actualización de una entrada en una transición
No se puede utilizar una transición para una variable de estado que controla una entrada:
const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ Can't use transitions for controlled input state
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;
Esto se debe a que las transiciones son no bloqueantes, pero la actualización de una entrada en respuesta al evento de cambio debe producirse de forma sincrónica. Si deseas ejecutar una transición en respuesta a la escritura, tiene dos opciones:
- Puedes declarar dos variables de estado separadas: una para el estado de la entrada (que siempre se actualiza de forma sincrónica), y otra que actualizarás en una transición. Esto te permite controlar la entrada utilizando el estado síncrono, y pasar la variable de estado de transición (que «irá por detrás» de la entrada) al resto de tu lógica de renderizado.
- Alternativamente, puedes tener una variable de estado, y añadir
useDeferredValue
que «irá por detrás» del valor real. Se activarán rerenderizados no bloqueantes para «ponerse al día» con el nuevo valor de forma automática.
React no trata mi actualización de estado como una transición
Cuando envuelvas una actualización de estado en una transición, asegúrate de que ocurre durante la llamada startTransition
:
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
La función que pases a startTransition
debe ser síncrona.
No puedes marcar una actualización como una transición así:
startTransition(() => {
// ❌ Setting state *after* startTransition call
setTimeout(() => {
setPage('/about');
}, 1000);
});
En su lugar, podrías hacer esto:
setTimeout(() => {
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
}, 1000);
Del mismo modo, no se puede marcar una actualización como una transición como esta:
startTransition(async () => {
await someAsyncFunction();
// ❌ Setting state *after* startTransition call
setPage('/about');
});
Sin embargo, esto funciona en su lugar:
await someAsyncFunction();
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
Quiero llamar a useTransition
desde fuera de un componente
No puedes llamar a useTransition
fuera de un componente porque es un Hook. En este caso, utiliza el método independiente startTransition
. Funciona de la misma manera, pero no proporciona el indicador isPending
.
La función que paso a startTransition
se ejecuta inmediatamente
Si ejecutas este código, imprimirá 1, 2, 3:
console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);
Se espera que imprima 1, 2, 3. La función que pasas a startTransition
no se retrasa. Al contrario que con el setTimeout
del navegador, no ejecuta el callback más tarde. React ejecuta tu función inmediatamente, pero cualquier actualización de estado programada mientras se está ejecutando se marca como transición. Puedes imaginar que funciona así:
// A simplified version of how React works
let isInsideTransition = false;
function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}
function setState() {
if (isInsideTransition) {
// ... schedule a transition state update ...
} else {
// ... schedule an urgent state update ...
}
}