El Estado: La memoria un componente

Los componentes a menudo necesitan cambiar lo que se muestra en pantalla como resultado de una interacci√≥n. Escribir dentro de un formulario deber√≠a actualizar el campo de texto, hacer clic en ‚Äúsiguiente‚ÄĚ en un carrusel de im√°genes deber√≠a cambiar la imagen que es mostrada; hacer click en un bot√≥n para comprar un producto deber√≠a actualizar el carrito de compras. En los ejemplos anteriores los componentes deben ‚Äúrecordar‚ÄĚ cosas: el campo de texto, la imagen actual, el carrito de compras. En React, a este tipo de memoria de los componentes se le conoce como estado.

Aprender√°s

  • C√≥mo agregar una variable de estado con el Hook de useState
  • Qu√© par de valores devuelve el hook de useState
  • C√≥mo agregar m√°s de una variable de estado
  • Por qu√© se le llama local al estado

Cuando una variable regular no es suficiente

Aqu√≠ hay un componente que renderiza una imagen de una escultura. Al hacer clic en el bot√≥n ‚ÄúSiguiente‚ÄĚ, deber√≠a mostrarse la siguiente escultura cambiando el √≠ndice index a 1, luego a 2, y as√≠ sucesivamente. Sin embargo, esto no funcionar√° (¬°puedes intentarlo!):

import { sculptureList } from './data.js';

export default function Gallery() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Siguiente
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        por {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} de {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

El controlador de eventos handleClick est√° actualizando una variable local, index. Pero dos cosas impiden que ese cambio sea visible:

  1. Las variables locales no persisten entre renderizaciones. Cuando React renderiza este componente por segunda vez, lo hace desde cero, no considera ning√ļn cambio en las variables locales.
  2. Los cambios en las variables locales no activar√°n renderizaciones. React no se da cuenta de que necesita renderizar el componente nuevamente con los nuevos datos.

Para actualizar un componente con datos nuevos, deben pasar las siguientes dos cosas:

  1. Conservar los datos entre renderizaciones.
  2. Provocar que React renderice el componente con nuevos datos (re-renderizado).

El Hook de useState ofrece dos cosas:

  1. Una variable de estado para mantener los datos entre renderizados.
  2. Una función que setea el estado para actualizar la variable y provocar que React renderice el componente nuevamente.

Agregar una variable de estado

Para agregar una variable de estado, debemos importar el useState de React al inicio del archivo:

import { useState } from 'react';

Luego, reemplazamos esta línea:

let index = 0;

con

const [index, setIndex] = useState(0);

index es una variable de estado y setIndex es la función que setea el estado.

La sintaxis de [ y ] se le conoce cómo desestructuración de un array y permite leer valores de un array. El array retornado por useState siempre contará con exactamente dos valores.

Así es como funcionan juntos en el handleClick:

function handleClick() {
setIndex(index + 1);
}

Ahora al hacer clic en el bot√≥n ‚ÄúSiguiente‚ÄĚ cambia la escultura actual:

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);

  function handleClick() {
    setIndex(index + 1);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Siguiente
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        por {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} de {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

Conoce tu primer Hook

En React, useState, as√≠ como cualquier otra funci√≥n que empiece con ‚ÄĚuse‚ÄĚ, se le conoce como Hook.

Los Hooks son funciones especiales que s√≥lo est√°n disponibles mientras React est√° renderizando (algo que veremos con m√°s detalle en la p√°gina siguiente). Los Hooks permiten ‚Äúengancharnos‚ÄĚ a diferentes caracter√≠sticas de React.

El estado es solo una de esas características, pero conoceremos los otros Hooks más adelante.

Atención

Las funciones-Hook que empiecen con use deben ser solo llamadas en el nivel superior de los componentes o en tus propios Hooks. No podemos usar Hooks dentro de condicionales, bucles, u otras funciones anidadas. Los Hooks son funciones, pero es √ļtil pensar en ellos como declaraciones independientes de las necesidades de nuestro componente. Las funciones de React se ‚Äúusan‚ÄĚ en la parte superior del componente de manera similar a c√≥mo se ‚Äúimportan‚ÄĚ m√≥dulos en la parte superior de un archivo.

Anatomía del useState

Cuando llamamos al useState, le estamos diciendo a React que debe recordar algo:

const [index, setIndex] = useState(0);

En este caso, queremos que React recuerde el index.

La convención es nombrar estas dos variables como const [algo, setAlgo]. Podemos nombrarlo como queramos, pero mantener las convenciones hacen que las cosas sean más fáciles de entender en todos los proyectos.

El √ļnico argumento para useState es el valor inicial de su variable de estado. En este ejemplo, el valor inicial de index se establece en 0 con useState(0).

Cada vez que el componente se renderiza, el useState devuelve un array que contiene dos valores:

  1. La variable de estado (index) con el valor que almacenaste.
  2. La función que establece el estado (setIndex) que puede actualizar la variable de estado y alertar a React para que renderice el componente nuevamente.

Así es como sucede en acción:

const [index, setIndex] = useState(0);
  1. Tu componente se renderiza por primera vez. Debido a que pasamos 0 a useState como valor inicial para index, esto devolver√° [0, setIndex]. React recuerda que 0 es el √ļltimo valor de estado.
  2. Actualizas el estado. Cuando un usuario hace clic en el botón, llama a setIndex(index + 1). index es 0, por lo tanto es setIndex(1). Esto le dice a React que recuerde que index es 1 ahora y ejecuta otro renderizado.
  3. El componente se renderiza por segunda vez. React todavía ve useState(0), pero debido a que React recuerda que estableció index en 1, devuelve [1, setIndex] en su lugar.
  4. ¡Y así sucesivamente!

Colocar m√ļltiples variables de estado a un componente

Podemos tener m√°s de una variable de estado de diferentes tipos en un componente. Este componente tiene dos variables de estado, un index num√©rico y un showMore booleano que se activa al hacer clic en ‚ÄúMostrar detalles‚ÄĚ:

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Siguiente
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        por {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} de {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Ocultar' : 'Mostrar'} detalles
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}

Es una buena idea tener m√ļltiples variables de estado si no se encuentran relacionadas entre s√≠, como index y showMore en este ejemplo. Pero si encontramos que a menudo cambiamos dos variables de estado juntas, podr√≠a ser mejor combinarlas en una sola. Por ejemplo, si tenemos un formulario con muchos campos, es m√°s conveniente tener una √ļnica variable de estado que contenga un objeto que una variable de estado por campo. Elegir la estructura del estado tiene m√°s consejos sobre esto.

Deep Dive

¬ŅC√≥mo sabe React qu√© estado devolver?

Es posible que hayas notado que la llamada a useState no recibe ninguna informaci√≥n sobre a qu√© variable de estado se refiere. No hay un ‚Äúidentificador‚ÄĚ que se pase a useState, entonces, ¬Ņc√≥mo sabe cu√°l de las variables de estado deber√≠a devolver? ¬ŅSe basa en alg√ļn tipo de magia para esto? La respuesta es no.

En cambio, para habilitar su sintaxis concisa, los Hooks se basan en un orden de llamada estable en cada representaci√≥n del mismo componente. Esto funciona bien en la pr√°ctica porque si seguimos la regla anterior (‚Äúsolo llamar a los Hooks en el nivel superior‚ÄĚ), los Hooks siempre se llamar√°n en el mismo orden. Adem√°s, este [complemento para el linter] (https://www.npmjs.com/package/eslint-plugin-react-hooks) detecta la mayor√≠a de los errores.

Internamente, React mantiene un array de pares de estados para cada componente. También mantiene el índice de par actual, el cual se establece en 0 antes de ser renderizado. Cada vez que llamamos a useState, React devuelve el siguiente par de estados e incrementa el índice. Puedes leer más sobre este mecanismo en React Hooks: No es magia, sólo son Arrays.

Este ejemplo no usa React pero nos da una idea de cómo funciona useState internamente:

let componentHooks = [];
let currentHookIndex = 0;

// Cómo funciona useState dentro de React (simplificado).
function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
    // Este no es el primer render,
    // entonces el par de estados ya existe.
    // Devuélvelo y prepárate para la próxima llamada del Hook.
    currentHookIndex++;
    return pair;
  }

  // Esta es la primera vez que estamos renderizando,
  // así que crea un array de dos posiciones y guárdalo.
  pair = [initialState, setState];

  function setState(nextState) {
    // Cuando el usuario solicita un cambio de estado,
    // guarda el nuevo valor en el par.
    pair[0] = nextState;
    updateDOM();
  }

  // Guarda el par para futuros renderizados
  // y se prepara para la siguiente llamada del Hook.
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

function Gallery() {
  // Cada llamada a useState() retornar√° el siguiente par.
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  // Este ejemplo no usa React, entonces
  // devuelve un objeto como resultado en lugar de JSX.
  return {
    onNextClick: handleNextClick,
    onMoreClick: handleMoreClick,
    header: `${sculpture.name} por ${sculpture.artist}`,
    counter: `${index + 1} of ${sculptureList.length}`,
    more: `${showMore ? 'Ocultar' : 'Mostrar'} detalles`,
    description: showMore ? sculpture.description : null,
    imageSrc: sculpture.url,
    imageAlt: sculpture.alt
  };
}

function updateDOM() {
  // Restablece el índice del Hook actual
  // antes de renderizar el componente.
  currentHookIndex = 0;
  let output = Gallery();

  // Actualiza el DOM para que coincida con el resultado.
  // Esta es la parte que React hace por ti.
  nextButton.onclick = output.onNextClick;
  header.textContent = output.header;
  moreButton.onclick = output.onMoreClick;
  moreButton.textContent = output.more;
  image.src = output.imageSrc;
  image.alt = output.imageAlt;
  if (output.description !== null) {
    description.textContent = output.description;
    description.style.display = '';
  } else {
    description.style.display = 'none';
  }
}

let nextButton = document.getElementById('nextButton');
let header = document.getElementById('header');
let moreButton = document.getElementById('moreButton');
let description = document.getElementById('description');
let image = document.getElementById('image');
export const sculptureList = [{
  name: 'Homenaje a la Neurocirugía',
  artist: 'Marta Colvin Andrade',
  description: 'Aunque Colvin es predominantemente conocida por temas abstractos que aluden a s√≠mbolos prehisp√°nicos, esta gigantesca escultura, un homenaje a la neurocirug√≠a, es una de sus obras de arte p√ļblico m√°s reconocibles.',
  url: 'https://i.imgur.com/Mx7dA2Y.jpg',
  alt: 'Una estatua de bronce de dos manos cruzadas sosteniendo delicadamente un cerebro humano con la punta de sus dedos.'  
}, {
  name: 'Floralis Genérica',
  artist: 'Eduardo Catalano',
  description: 'Esta enorme flor plateada (75 pies o 23 m) se encuentra en Buenos Aires. Est√° dise√Īado para moverse, cerrando sus p√©talos por la tarde o cuando soplan fuertes vientos y abri√©ndolos por la ma√Īana.',
  url: 'https://i.imgur.com/ZF6s192m.jpg',
  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'
}, {
  name: 'Eternal Presence',
  artist: 'John Woodrow Wilson',
  description: 'Wilson era conocido por su preocupación por la igualdad y la justicia social, así como por las cualidades esenciales y espirituales de la humanidad. Esta enorme pieza de bronce (de 7 pies o 2,13 metros) representa lo que él describió como "una presencia negra simbólica impregnada de un sentido de humanidad universal".',
  url: 'https://i.imgur.com/aTtVpES.jpg',
  alt: 'La escultura que representa una cabeza humana parece omnipresente y solemne. Irradia calma y serenidad.'
}, {
  name: 'Moai',
  artist: 'Artista Desconocido',
  description: 'Ubicados en la Isla de Pascua, hay 1000 moai, o estatuas monumentales existentes, creadas por los primeros Rapa Nui, que algunos creen que representaban a ancestros deificados.',
  url: 'https://i.imgur.com/RCwLEoQm.jpg',
  alt: 'Tres bustos monumentales de piedra con las cabezas desproporcionadamente grandes con rostros sombríos.'
}, {
  name: 'Blue Nana',
  artist: 'Niki de Saint Phalle',
  description: 'Las Nanas son criaturas triunfantes, símbolos de feminidad y maternidad. En un principio, Saint Phalle utilizaba telas y objetos encontrados para las Nanas, y más tarde introdujo el poliéster para conseguir un efecto más vibrante.',
  url: 'https://i.imgur.com/Sd1AgUOm.jpg',
  alt: 'Una gran escultura de mosaico de una caprichosa figura femenina bailando con un colorido traje que emana alegría.'
}, {
  name: 'Ultimate Form',
  artist: 'Barbara Hepworth',
  description: 'Esta escultura abstracta de bronce es parte de la serie The Family of Man ubicada en Yorkshire Sculpture Park. Hepworth optó por no crear representaciones literales del mundo, sino que desarrolló formas abstractas inspiradas en personas y paisajes.',
  url: 'https://i.imgur.com/2heNQDcm.jpg',
  alt: 'Una escultura alta formada por tres elementos apilados unos sobre otros que recuerdan una figura humana.'
}, {
  name: 'Cavaliere',
  artist: 'Lamidi Olonade Fakeye',
  description: "Descendiente de cuatro generaciones de talladores de madera, el trabajo de Fakeye combinó temas Yoruba tradicionales y contemporáneos.",
  url: 'https://i.imgur.com/wIdGuZwm.png',
  alt: 'Una intrincada escultura de madera de un guerrero con el rostro centrado en un caballo adornado con patrones.'
}, {
  name: 'Big Bellies',
  artist: 'Alina Szapocznikow',
  description: "Szapocznikow es conocida por sus esculturas del cuerpo fragmentado como met√°fora de la fragilidad y la impermanencia de la juventud y la belleza. Esta escultura representa dos vientres grandes muy realistas apilados uno encima del otro, cada uno de unos cinco pies (1,5 m) de altura.",
  url: 'https://i.imgur.com/AlHTAdDm.jpg',
  alt: 'La escultura recuerda una cascada de pliegues, muy diferente a los vientres de las esculturas cl√°sicas.'
}, {
  name: 'Terracotta Army',
  artist: 'Artista Desconocido',
  description: 'El Ejército de terracota es una colección de esculturas de terracota que representan los ejércitos de Qin Shi Huang, el primer emperador de China. El ejército constaba de más de 8.000 soldados, 130 carros con 520 caballos y 150 caballos de caballería.',
  url: 'https://i.imgur.com/HMFmH6m.jpg',
  alt: '12 esculturas de terracota de guerreros solemnes, cada uno con una expresi√≥n facial y una armadura √ļnicas.'
}, {
  name: 'Lunar Landscape',
  artist: 'Louise Nevelson',
  description: 'Nevelson era conocida por recoger objetos de los escombros de la ciudad de Nueva York, que luego ensamblaría en construcciones monumentales. En este, usó partes dispares como un poste de la cama, un alfiler de malabares y un fragmento de asiento, clavándolos y pegándolos en cajas que reflejan la influencia de la abstracción geométrica del espacio y la forma del cubismo.',
  url: 'https://i.imgur.com/rN7hY6om.jpg',
  alt: 'Una escultura negra mate donde los elementos individuales son inicialmente indistinguibles.'
}, {
  name: 'Aureole',
  artist: 'Ranjani Shettar',
  description: 'Shettar fusiona lo tradicional y lo moderno, lo natural y lo industrial. Su arte se centra en la relación entre el hombre y la naturaleza. Su trabajo fue descrito como convincente tanto abstracta como figurativamente, desafiando la gravedad, y una "fina síntesis de materiales inverosímiles.',
  url: 'https://i.imgur.com/okTpbHhm.jpg',
  alt: 'Una escultura parecida a un alambre montado en una pared de hormigón que desciende al suelo. Parece ligero.'
}, {
  name: 'Hippos',
  artist: 'Taipei Zoo',
  description: 'El Zoológico de Taipei realizó una Zona de Hipopótamos con hipopótamos sumergidos jugando.',
  url: 'https://i.imgur.com/6o5Vuyu.jpg',
  alt: 'Un grupo de esculturas de hipopótamos de bronce que emergen de la acera como si estuvieran nadando.'
}];

// Hacemos que la interfaz de usuario coincida con el estado inicial..
updateDOM();

No es necesario que lo entiendas para usar React, pero podr√≠as encontrarlo como un modelo mental √ļtil.

El estado es aislado y privado.

El estado es local para una instancia de un componente en la pantalla. En otras palabras, si renderizas el mismo componente dos veces, ¬°cada copia tendr√° un estado completamente aislado! Cambiar uno de ellos no afectar√° al otro.

En este ejemplo, el anterior componente de Galería se ha renderizado dos veces sin cambios en su lógica. Puedes intentar hacer clic en los botones dentro de cada una de las galerías. Observarás que su estado es independiente:

import Gallery from './Gallery.js';

export default function Page() {
  return (
    <div className="Page">
      <Gallery />
      <Gallery />
    </div>
  );
}

Esto es lo que hace que el estado sea diferente de las variables regulares que declaramos en la parte superior de un m√≥dulo. El estado no est√° vinculado a una llamada de funci√≥n en particular o a un lugar en el c√≥digo, pero es ‚Äúlocal‚ÄĚ al lugar espec√≠fico en la pantalla. Se han renderizado dos componentes <Gallery />, por lo que su estado se almacena por separado.

Tambi√©n observemos c√≥mo el componente de la p√°gina Page no ‚Äúsabe‚ÄĚ nada sobre el estado del componente de la galer√≠a Galery, ni siquiera si es que posee alg√ļn estado. A diferencia de las props, el estado es totalmente privado para el componente que lo declara. El componente padre no puede cambiarlo. Esto permite agregar el estado a cualquier componente o eliminarlo sin afectar al resto de los componentes.

¬ŅQu√© pasar√≠a si quisieramos que ambas galer√≠as mantuvieran sus estados sincronizados? La forma correcta de hacerlo en React es eliminar el estado de los componentes secundarios y agregarlo a su padre m√°s cercano. Las pr√≥ximas p√°ginas se centrar√°n en organizar el estado de un solo componente, pero volveremos a este tema en Compartir estado entre componentes.

Recapitulación

  • Debemos utilizar una variable de estado cuando necesitamos que un componente necesite ‚Äúrecordar‚ÄĚ alguna informaci√≥n entre renderizaciones.
  • Las variables de estado se declaran llamando al Hook useState.
  • Los Hooks son funciones especiales que comienzan con use. Nos permiten ‚Äúenlazarnos‚ÄĚ a funciones de React como el estado.
  • Evita llamar a Hooks de manera anidada (por ejemplo, dentro de bucles o condicionales). Llamar a Hooks -incluyendo al useState- solo es v√°lido en el nivel superior de un componente u otro Hook.
  • El Hook useState retorna un array de dos valores: el estado actual y la funci√≥n para actualizarlo.
  • Puede tener m√°s de una variable de estado. Internamente, React los empareja por orden.
  • El estado es privado para un componente. Si los renderizamos en dos lugares, cada componente lo maneja individualmente.

Cuando presionamos ‚ÄúSiguiente‚ÄĚ en la √ļltima escultura, el c√≥digo falla. Arregla la l√≥gica para evitar el bloqueo. Puedes hacer esto agregando l√≥gica adicional al controlador de eventos o deshabilitando el bot√≥n cuando la acci√≥n no es posible.

Despu√©s de arreglar el error, agrega un bot√≥n ‚ÄúAnterior‚ÄĚ que muestre la escultura anterior. No deber√≠a chocar con la primera escultura.

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Siguiente
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        por {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} de {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Ocultar' : 'Mostrar'} detalles
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}