Skip to main content

Command Palette

Search for a command to run...

useTransition

Controlar las transiciones de la interfaz de usuario de manera más efectiva desde React 18

Updated
10 min read
useTransition
D

Desarrollando código desde los 80s desde que en los 80 vi por primera vez un Amstrad en un escaparate de una tienda de electrónica en mi ciudad natal, lo que me llevó a insistirles a mis padres para que me apuntaran a clases de informática donde aprendí cosas como Basic y ensamblador del x86.

A lo largo de mi carrera profesional he pasado por varias revoluciones (y crisis) que han hecho que me haya tenido que ir reinventando para poder seguir desarrollando mi trabajo de la mejor forma posible.

Creo que el desarrollo de software tiene una parte de ciencia pero también una de artesanía (y creatividad) que lo convierten en algo apasionante.

Eternamente contagiado con el Síndrome del Impostor.

Es más que probable que cuando estamos desarrollando una aplicación nos encontremos en una situación en la que tengamos un enlace que nos lleve a una nueva ruta dentro de nuestro sitio en la que se mostrará la información que se obtiene tras consultar una API.

No solamente eso sino que además es más que probable que nos pase algo parecido a lo que podemos ver en la siguiente imagen donde al pulsar sobre el enlace Post vemos que la respuesta de la interfaz no es inmediata sino que tarda unos segundos en obtener la información para poder construir la página y posteriormente la muestra.

El componente con el que estamos trabajando sería algo tan sencillo como lo siguiente donde únicamente mostramos aquellos elementos que nos servirán para explicar el hook que estamos estudiando.

type Tab = 'about' | 'contact' | 'posts'

export const Page = () => {
  const [tab, setTab] = useState<Tab>('about')

  return (
    <div className='tabs'>
      <div className='mb-4 flex flex-row items-center gap-4'>
        <Tab
          onClick={() => setTab('about')}
          title='About'
          variant={ tab === 'about' ? 'primary' : 'secondary' }
        />
        <Tab
          onClick={() => setTab('posts')}
          title='Posts'
          variant={ tab === 'posts' ? 'primary' : 'secondary' }
        />
        <Tab
          onClick={() => setTab('contact')}
          title='Contact'
          variant={ tab === 'contact' ? 'primary' : 'secondary' }
        />
      </div>

      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </div>
  )
}

A parte de lo que hemos comentado anteriormente aquí aparece un segundo problema que no tiene por qué resultar tan obvio de ver. Supongamos que en esta aplicación estamos en la pestaña About y pulsamos sobre el botón Posts lo que desencadenará la llamada a la API para poder traerse la información de los posts que serán mostrados en la página. Y ahora es cuando tenemos que estar atentos puesto que es posible que, antes de que se rendericen los post el usuario pulse sobre la tab Contact pero esto no va a ser posible puesto que no se han terminado de renderizar los posts lo que desconcertará a nuestro usuario.

De hecho solamente en el momento en el que se acaba de obtener toda la información que va a permitir renderizar la página de los posts es cuando de forma inmediata se va a renderizar la página con la información de contacto.

En definitiva estaremos dando una mala experiencia de usuario puesto que la interfaz estará bloqueada hasta que no se acaben de obtener todos los posts aunque no se vayan a mostrar en ningún resultado.

useTransition

La forma que tenemos de evitar este problema es mediante el uso de hook useTransition que es un hook que le viene a decir a React que alguna de las actualizaciones que se están llevando a cabo en la UI de nuestras aplicaciones no son tan importantes y por lo tanto que van a poder ser interrumpidas por cualquier otra actualización que sí que tenga una mayor prioridad.

Volviendo a nuestro ejemplo en el caso de que pulsemos la secuencia de botones Posts y posteriormente About tenemos que pensar que es este segundo click (la pulsación sobre About) la que va a tener más prioridad sobre la pulsación en Posts (el usuario quiere ver la información que está contenida en la sección about dejando de estar interesado en los posts).

Así pues nuestra intención va a ser indicar en nuestra aplicación que el renderizado de los posts no es tan importante como bloquear la UI y esto lo lograremos gracias al uso de useTransition.

Como sucede con cualquier otro hook con el que estemos trabajando lo primero que tendremos que hacer será importarlo en nuestro código:

import { useState, useTransition } from 'react'

y ahora pasamos a utilizarlo dentro de nuestro componente sabiendo que la invocación useTranstion va a devolver un array formado por dos elementos a los que se les suelen llamar isPending y startTransition respectivamente:

export const Page = () => {
  const [isPending, startTransition] = useTransition()
  // ...
}

Ahora lo que vamos a hacer es definir una nueva función que se encargará de establer la pestaña en la que nos encontraremos dentro de la aplicación y que pasará a ser la que llamarán los eventos click de los botones que forman parte de nuestra interfaz de usuario:

export const Page = () => {
  const [isPending, startTransition] = useTransition()
  const [tab, setTab] = useState<Tab>('about')

  const handleClickTab = (tab: Tab) => {
    setTab(tab)
  }

  return (
    <div className='tabs'>
      <div className='mb-4 flex flex-row items-center gap-4'>
        <Tab
          onClick={() => handleClickTab('about')}
          title='About'
          variant={ tab === 'about' ? 'primary' : 'secondary' }
        />
    ...

Dentro de esta nueva función lo que vamos a hacer es utilizar la función startTransition que nos ha proporcionado useTransition la cual espera recibir como parámetro la función que queremos que sea ejecutada con esa prioridad baja (o dicho de otra manera, sin bloquear la interfaz de usuario). En nuestro ejemplo esta función no será otra que la encargada de establecer la pestaña que se ha de mostrar.

const handleClickTab = (tab: Tab) => {
  startTranstion(() => {
    setTab(tab)
  })
}

y simplemente con este cambio ya estaremos logrando nuestro objetivo puesto que si ahora nos vamos a Posts y a continuación a Contacts veremos que el cambio en la interfaz se produce de forma automática como esperábamos:

Importante: esto no quiere decir que en ningún momento se muestre la información de todos los posts que forman parte de la aplicación puesto que si pulsamos en Posts y no interactuamos con ningún otro botón vamos a ver cómo el funcionamiento es el que se esperaba aunque la interfaz tardará en ser renderizada ya que precisa la información de los post.

El código completo de nuestro componente quedará por lo tanto como se puede ver a continuación:

import { useState, useTransition } from 'react'

import { AboutTab } from './AboutTab'
import { ContactTab } from './ContactTab'
import { PostsTab } from './PostsTab'
import { Tab } from './Tab'

type Tab = 'about' | 'contact' | 'posts'

export const Page = () => {
  const [isPending, startTransition] = useTransition()
  const [tab, setTab] = useState<Tab>('about')

  const handleClickTab = (tab: Tab) => {
    startTranstion(() => {
      setTab(tab)
    })
  }

  return (
    <div className='tabs'>
      <div className='mb-4 flex flex-row items-center gap-4'>
        <Tab
          onClick={() => handleClickTab('about')}
          title='About'
          variant={ tab === 'about' ? 'primary' : 'secondary' }
        />
        <Tab
          onClick={() => handleClickTab('posts')}
          title='Posts'
          variant={ tab === 'posts' ? 'primary' : 'secondary' }
        />
        <Tab
          onClick={() => handleClickTab('contact')}
          title='Contact'
          variant={ tab === 'contact' ? 'primary' : 'secondary' }
        />
      </div>

      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </div>
  )
}

Otros casos de uso

Otra situación en la que puede ser interesante hacer uso de useTransition podría ser dentro de un hipotético botón que tengamos dentro de nuestra aplicación y que lo estemos reutilizando en varias partes de la misma. El código de partida de nuestro botón podría ser algo como lo siguiente:

import Button, { ButtonProps } from '@/components/ui/Button'

interface TabButton extends ButtonProps {}

export const Tab = (props: TabButtonProps) => (
  <Button {...props} />
)

Si ahora pasamos a utilizar useTransition dentro de nuestro componente de forma similar a como lo hemos hecho en el ejemplo anterior:

export const Tab = (props: TabButtonProps) => {
  const [isLoading, startTransition] = useTransition()

  return <Button {...props} />
}

y ahora lo que vamos a hacer es definir la función que se encargará de manejar los clicks que se puedan llevar a cabo en estos botones para que pasen a hacer uso de una nueva transición de React cuando se realicen:

const handleClick = () => {
  startTransition(() => {
    // ....
  })
}

pero ¿qué tendremos que hacer dentro de la función que se recibe como parámetro de startTransition? Pues simplemente deberíamos llamar a la función que se le haya pasado como prop a componente Tab y que tendrá el callback que queremos que se ejecute cuando se pulse sobre el botón. Esto nos llevará que tendremos que destructurar las props que recibe nuestro componente:

export const Tab = ({ onClick, ...rest }: TabButtonProps) => {

y ahora simplemente sabiendo la prop onClick puede o no estar presente (es opcional) dentro la función que recibe startTransition la invocaremos siempre que esté presente:

const handleClick = () => {
  startTransition(() => {
    onClick?.()
  })
}

y como ahora estamos gestionando el click en el botón dentro de startTransition lo que vamos a hacer es no pasar esta prop a componente Button que se encargará de renderizar el botón pero sí el resto de la props que se estuviesen recibiendo junto con la prop onClick a la que le asignaremos la función handleClick que acabamos de definir:

return <Button {...rest} />

Si ahora vemos el código completo del componente Tab es mucho más sencillo entender todas las modificaciones que hemos llevado a cabo:

import { useTransition } from 'react'
import Button, { ButtonProps } from '@/components/ui/Button'

interface TabButton extends ButtonProps {}

export const Tab = ({ onClick, ...rest }: TabButtonProps) => {
  const [isLoading, startTransition] = useTransition()

  const handleClick = () => {
    startTransition(() => {
      onClick?.()
    })
  } 

  return <Button {...rest} onClick={handleClick}/>
}

Con esta optimización podemos quitar todo el código que tiene que ver con useTranstion en el componente que lo vaya a utilizar porque ahora los click en estos elementos son gestionados como transiciones de baja prioridad pero dentro del prop Tab.

Es decir, que ahora nuestro componente Page ya no tiene que precouparse por la gestión de las transiciones de baja prioridad y por lo tanto podemos eliminar del mismo todo el código que estaba relacionado con ello dejándonos algo como lo que se puede ver a continuación:

import { useState } from 'react'

import { AboutTab } from './AboutTab'
import { ContactTab } from './ContactTab'
import { PostsTab } from './PostsTab'
import { Tab } from './Tab'

type Tab = 'about' | 'contact' | 'posts'

export const Page = () => {
  const [tab, setTab] = useState<Tab>('about')

  return (
    <div className='tabs'>
      <div className='mb-4 flex flex-row items-center gap-4'>
        <Tab
          onClick={() => setTab('about')}
          title='About'
          variant={ tab === 'about' ? 'primary' : 'secondary' }
        />
        <Tab
          onClick={() => setTab('posts')}
          title='Posts'
          variant={ tab === 'posts' ? 'primary' : 'secondary' }
        />
        <Tab
          onClick={() => setTab('contact')}
          title='Contact'
          variant={ tab === 'contact' ? 'primary' : 'secondary' }
        />
      </div>

      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </div>
  )
}

Por lo tanto startTrasition es una función que nos va a permitir interaccionar con el estado de los componentes que forman parte de la interfaz de usuario incluso en los casos en los que se gestione el estado de un componente padre en un componente hijo recibiendo la función de actualización como una prop del hijo (caso que hemos visto en el ejemplo del componente Tab).

isPending

Otro de los beneficios que nos ofrece utilizar useTranstion es utilizar la variable isPending que hemos visto que este retorne en el primero de los elementos del array con los que responde su invocación. Lo que tenemos que entender es que esta es una variable de tipo boolean que adopta el valor true cuando hay una transición que todavía está en progreso (por ejemplo, cuando todavía nos estamos trayendo los posts con los que componer nuestra página).

Así un uso más que habitual de ella es mostrar un mensaje mientras la transición está pendiente. Por ejemplo, en nuestro componente Tab lo que vamos a hacer es mostrar un mensaje en el caso de que la transición esté pendiente o el botón en el caso de que no la haya. En definitiva modificaremos el código como sigue:

import { useTransition } from 'react'
import Button, { ButtonProps } from '@/components/ui/Button'

interface TabButton extends ButtonProps {}

export const Tab = ({ onClick, ...rest }: TabButtonProps) => {
  const [isPending, startTransition] = useTransition()

  const handleClick = () => {
    startTransition(() => {
      onClick?.()
    })
  } 

  return isPending ? <p>Loading...</p> : <Button {...rest} onClick={handleClick}/>
}

Con esta modificación cuando pulsemos sobre el botón Posts nos vamos a encontrar con que se mostrará el mensaje Loading... mientras no se termine de traer todos los posts con los que se puede componente la página:

Vemos por lo tanto que el uso de isPending nos ayudará a ofrecer una mejor experiencia de usuario a los usuarios de nuestra aplicación puesto que gracias a que se está mostrando el mensaje de carga el usuario no pensará que nuestra interfaz se ha quedado bloqueada y por lo tanto que nuestra aplicación habrá dejado de funcionar.

React

Part 1 of 14

En esta serie de artículos describiremos los principios y técnicas que deberemos saber para poder trabajar de forma efectiva con React.

Up next

useDeferredValue

Diferir el valor de una variable de estado en un componente funcional en React 18