useTransition es un Hook de React que te permite actualizar el estado sin bloquear la UI.

const [isPending, startTransition] = useTransition()

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();
// ...
}

Ver más ejemplos abajo.

Parámetros

useTransition no recibe ningun parámetro.

Devuelve

useTransition devuelve un array con exactamente dos elementos:

  1. isPending que indica si hay una transición pendiente.
  2. 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

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 independiente startTransition 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, prueba useDeferredValue 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:

  1. isPending flag que te indica si hay una transición pendiente.
  2. 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.

Diferencia entre useTransition y las actualizaciones de estado normales

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.

Nota

Las transiciones sólo «esperarán» el tiempo suficiente para evitar ocultar el contenido ya revelado (como el contenedor de la pestaña). Si la pestaña Entradas tuviera una barrera de <Suspense> anidada, la transición no la «esperaría».


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:

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>;
}

Nota

Se espera que los enredadores preparados para Suspense envuelvan las actualizaciones de navegación en transiciones por defecto.


Displaying an error to users with a error boundary

Canary

La barrera de error para useTransition está disponible actualmente solo en los canales experimental y canary de React. Más información sobre los canales de lanzamiento de React aquí.

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:

  1. 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.
  2. 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 ...
}
}