Reaccionar a las entradas con el estado

React utiliza una forma declarativa para manipular la UI. En vez de manipular trozos de la UI de forma individual directamente, describes los diferentes estados en los que puede estar tu componente, y cambias entre ellos en respuesta al lo que haga el usuario. Esto es similar a como los diseñadores piensan en la UI

Aprenderás

  • Como la programación de UI declarativa se diferencia de la programación de UI imperativa
  • Como enumerar los diferentes estados visuales en los que tus componentes pueden estar
  • Como forzar los cambios entre los distintos estados desde el código

Cómo la UI declarativa se compara a la imperativa

Cuando diseñas interacciones con la UI, seguramente pensarás en como la UI cambia en respuesta a las acciones del usuario. Imagina un formulario que permita al usuario enviar una respuesta:

  • Cuando escribes algo en el formulario, el botón «Enviar» se habilita.
  • Cuando presionas «Enviar», tanto el formulario como el botón se deshabilitan, y un indicativo de carga aparece.
  • Si la petición es exitosa, el formulario se oculta, y un mensaje «Gracias» aparece.
  • Si la petición falla, un mensaje de error aparece, y el formulario se habilita de nuevo.

En la programación imperativa, lo descrito arriba se corresponde directamente con como implementas la interacción. Tienes que escribir las instrucciones exactas para manipular la UI dependiendo de lo que acabe de suceder. Esta es otra manera de abordar este concepto: imagina acompañar a alguien en un coche mientras le dices paso a paso que tiene que hacer.

En un coche conducido por una persona con apariencia ansiosa, representando a JavaScript, un pasajero le ordena al conductor a realizar una complicada secuencia de giros e indicaciones.

Ilustrado por Rachel Lee Nabors

No sabe a donde quieres ir, solo sigue tus indicaciones. (Y si le das las indicaciones incorrectas, ¡acabarás en el lugar equivocado!) Se llama imperativo por que tienes que «mandar» a cada elemento, desde el indicativo de carga hasta el botón, diciéndole al ordenador cómo tiene que actualizar la UI.

En este ejemplo de UI declarativa, el formulario esta construido sin React. Utiliza el DOM del navegador:

async function handleFormSubmit(e) {
  e.preventDefault();
  disable(textarea);
  disable(button);
  show(loadingMessage);
  hide(errorMessage);
  try {
    await submitForm(textarea.value);
    show(successMessage);
    hide(form);
  } catch (err) {
    show(errorMessage);
    errorMessage.textContent = err.message;
  } finally {
    hide(loadingMessage);
    enable(textarea);
    enable(button);
  }
}

function handleTextareaChange() {
  if (textarea.value.length === 0) {
    disable(button);
  } else {
    enable(button);
  }
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

function enable(el) {
  el.disabled = false;
}

function disable(el) {
  el.disabled = true;
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (answer.toLowerCase() === 'istanbul') {
        resolve();
      } else {
        reject(new Error('Buen intento, pero incorrecto. ¡Inténtalo de nuevo!'));
      }
    }, 1500);
  });
}

let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;

Manipular la UI de forma imperativa funciona lo suficientemente bien en ejemplos aislados, pero se vuelve mas complicado de manejar de forma exponencial en sistemas complejos. Imagina actualizar una pagina llena de formularios diferentes como este. Añadir un elemento nuevo a la UI o una nueva interacción requeriría revisar todo el código existente meticulosamente para asegurarse de no haber introducido un bug (por ejemplo, olvidando mostrar u ocultar algo).

React fue construido para solucionar este problema.

En React, no necesitas manipular directamente la UI,lo que significa que no necesitas habilitar, deshabilitar, mostrar, u ocultar los componentes directamente. En su lugar, tú declaras lo que quieres mostrar, y React averigua cómo actualizar la UI. Piensa en ello como montarte en un taxi y decirle al conductor a donde quieres ir en lugar de decirle paso por paso que hacer. Es el trabajo del conductor llevarte a tu destino, ¡e incluso conocerá algún atajo que no habías considerado!

En un coche conducido por React, un pasajero indica el lugar al que desea ir en el mapa. React sabe como hacerlo.

Ilustrado por Rachel Lee Nabors

Pensar en la UI de forma declarativa

Arriba has visto como implementar un formulario de forma imperativa. Para entender mejor como pensar en React, recorrerás el ejemplo reimplementando esta UI en React más abajo:

  1. Identifica los diferentes estados visuales de tu componente
  2. Determina qué produce esos cambios de estado
  3. Representa el estado en memoria usando useState
  4. Elimina cualquier variable de estado no esencial
  5. Conecta los controladores de eventos para actualizar el estado

Paso 1: Identifica los diferentes estados visuales de tu componente

En las ciencias de la computación, tal vez escucharás algo sobre una «máquina de estado» siendo este uno de muchos «estados». Si trabajas con un diseñador, habrás visto bocetos para diferentes «estados visuales». React se encuentra en un punto intermedio de diseño y ciencias de la computación, asi que ambas ideas son fuentes de inspiración.

Primero, necesitas visualizar todos los diferentes «estados» de la UI que el usuario pueda ver:

  • Vacío: El formulario tiene deshabilitado el botón «Enviar».
  • Escribiendo: El formulario tiene habilitado el botón «Enviar».
  • Enviando: El formulario está completamente deshabilitado. Se muestra un indicador de carga.
  • Éxito: El mensaje «Gracias» se muestra en lugar del formulario.
  • Error: Igual que el estado de Escribiendo, pero con un mensaje de error extra.

Al igual que un diseñador, querrás «esbozar» o crear «bocetos» para los diferentes estados antes de añadir tu lógica. Por ejemplo, aquí hay un boceto solo para la parte visual del formulario. Este boceto es controlado por una prop llamado status con valor por defecto de 'empty':

export default function Form({
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>¡Correcto!</h1>
  }
  return (
    <>
      <h2>Cuestionario sobre ciudades</h2>
      <p>
       ¿En qué ciudad hay un cartel que convierte el aire en agua potable?
      </p>
      <form>
        <textarea />
        <br />
        <button>
          Enviar
        </button>
      </form>
    </>
  )
}

Podrías llamar a ese prop de la forma que quisieras, el nombre no es importante. Prueba a editar status = 'empty' a status = 'success' para que veas el mensaje aparecer. Esbozar te permita iterar en la UI rápidamente antes de comenzar con la lógica. Aquí hay una versión algo más desarrollada del mismo componente, todavía «controlada» por la prop status:

export default function Form({
  // Try 'submitting', 'error', 'success':
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>¡Correcto!</h1>
  }
  return (
    <>
      <h2>Cuestionario sobre ciudades</h2>
      <p>
        ¿En qué ciudad hay un cartel que convierte el aire en agua potable?
      </p>
      <form>
        <textarea disabled={
          status === 'submitting'
        } />
        <br />
        <button disabled={
          status === 'empty' ||
          status === 'submitting'
        }>
          Enviar
        </button>
        {status === 'error' &&
          <p className="Error">
            Buen intento, pero incorrecto. ¡Intntalo de nuevo!
          </p>
        }
      </form>
      </>
  );
}

Profundizar

Mostrar muchos estados visuales a la vez

Si un componente tiene un montón de estados visuales, puede resultar conveniente mostrarlos todos en una página:

import Form from './Form.js';

let statuses = [
  'empty',
  'typing',
  'submitting',
  'success',
  'error',
];

export default function App() {
  return (
    <>
      {statuses.map(status => (
        <section key={status}>
          <h4>Form ({status}):</h4>
          <Form status={status} />
        </section>
      ))}
    </>
  );
}

Páginas como estas son comúnmente llamadas como «guías de estilo en vivo» o «storybooks».

Paso 2: Determina qué produce esos cambios de estado

Puedes desencadenar actualizaciones de estado en respuesta a dos tipos de entradas:

  • Entradas humanas, como hacer click en un botón, escribir en un campo, navegar a un link.
  • Entradas del ordenador, como recibir una respuesta del navegador, que se complete un timeout, una imagen cargando.
Un dedo.
Entradas humanas
Unos y ceros.
Entradas del ordenador

Ilustrado por Rachel Lee Nabors

En ambos casos, debes declarar variables de estado para actualizar la UI. Para el formulario que vas a desarrollar, necesitarás cambiar el estado en respuesta de diferentes entradas:

  • Cambiar la entrada de texto (humano) debería cambiar del estado Vacío al estado Escribiendo o al revés, dependiendo de si la caja de texto está vacía o no.
  • Hacer click el el botón Enviar (humano) debería cambiarlo al estado Enviando .
  • Una respuesta exitosa de red (ordenador) debería cambiarlo al estado Éxito.
  • Una respuesta fallida de red (ordenador) debería cambiarlo al estado Error con el mensaje de error correspondiente.

Nota

¡Ten en cuenta que las entradas humanas suelen necesitar controladores de eventos!

Para ayudarte a visualizar este flujo, intenta dibujar cada estado en papel como un círculo etiquetado, y cada cambio entre dos estados como una flecha. Puedes esbozar muchos flujos de esta manera y ordenar los errores mucho antes de la implementación.


Diagrama de flujo que se mueve de izquierda a derecha con 5 nodos. El primer nodo etiquetado 'vacío' tiene una arista etiquetada 'empezar a escribir' conectada a un nodo etiquetado 'escribiendo'. Ese nodo tiene una arista etiquetada 'presionar enviar' conectada a un nodo etiquetado 'enviando', que tiene dos aristas. La arista izquierda está etiquetada 'error de red' conectada a un nodo etiquetado 'error'. La arista derecha está etiquetada 'éxito de red' conectada a un nodo etiquetado 'éxito'.

Diagrama de flujo que se mueve de izquierda a derecha con 5 nodos. El primer nodo etiquetado 'vacío' tiene una arista etiquetada 'empezar a escribir' conectada a un nodo etiquetado 'escribiendo'. Ese nodo tiene una arista etiquetada 'presionar enviar' conectada a un nodo etiquetado 'enviando', que tiene dos aristas. La arista izquierda está etiquetada 'error de red' conectada a un nodo etiquetado 'error'. La arista derecha está etiquetada 'éxito de red' conectada a un nodo etiquetado 'éxito'.

Estados del formulario

Paso 3: Representa el estado en memoria usando useState

A continuación, necesitarás representar los estados visuales de tu componente en la memoria con useState. La simplicidad es clave: cada pieza de estado es una «pieza en movimiento», y quieres tan pocas «piezas en movimiento» como sea posible. ¡Más complejidad conduce a más errores!

Comienza con el estado que absolutamente debe estar allí. Por ejemplo, necesitarás almacenar la respuesta para la entrada y el error (si existe) para almacenar el último error:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

Después, necesitarás una variable de estado que represente cuál de los estados visuales descritos anteriormente quieres mostrar. Generalmente hay más de una manera de representarlo en la memoria, por lo que necesitarás experimentar con ello.

Si tienes dificultades para pensar en la mejor manera inmediatamente, comienza agregando suficiente estado para que definitivamente estés seguro de que todos los posibles estados visuales están cubiertos:

const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

Tu primera idea probablemente no sea la mejor, ¡pero está bien! ¡Refactorizar el estado es parte del proceso!

Paso 4: Elimina cualquier variable de estado no esencial

Deberías evitar la duplicación en el contenido del estado, por lo que solo rastrearás lo que es esencial. Dedicar un poco de tiempo a refactorizar su estructura de estado hará que tus componentes sean más fáciles de entender, reducirá la duplicación y evitará significados no deseados. Tu objetivo es prevenir los casos en los que el estado en la memoria no represente ninguna UI válida que te gustaría que viera un usuario. (Por ejemplo, nunca deberías mostrar un mensaje de error y deshabilitar la entrada al mismo tiempo, ¡o el usuario no podría corregir el error!)

Aquí hay algunas preguntas que podrías hacerte sobre tus variables de estado:

  • ¿Significa que el estado causa un paradoja? Por ejemplo, isTyping y isSubmitting no pueden ser ambos true. Un paradoja generalmente significa que el estado no está lo suficientemente restringido. Hay cuatro combinaciones posibles de dos booleanos, pero solo tres corresponden a estados válidos. Para eliminar el estado «imposible», puede combinarlos en un status que debe ser uno de tres valores: 'typing', 'submitting', o 'success'.
  • ¿La misma información está disponible en otra variable de estado ya? Otra paradoja: isEmpty y isTyping no pueden ser true al mismo tiempo. Al hacerlos variables de estado separadas, corre el riesgo de que se desincronicen y causen errores. Afortunadamente, se puede eliminar isEmpty y en su lugar verificar answer.length === 0.
  • ¿Se puede obtener la misma información de la inversa de otra variable de estado? isError no es necesario porque se puede comprobar error !== null en su lugar.

Después de esta limpieza, nos quedamos con 3 (¡a partir de 7!) variables de estado esenciales :

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', o 'success'

Sabes que son esenciales, porque no puedes eliminar ninguna de ellos sin romper la funcionalidad.

Profundizar

Eliminar estados «imposibles» con un reducer

Estas tres variables son una representación suficientemente buena del estado de este formulario. Sin embargo, todavía hay algunos estados intermedios que no tienen sentido. Por ejemplo, un error no nulo no tiene sentido cuando status es 'success'. Para modelar el estado con más precisión, puedes extraerlo en un reducer. ¡Los reducers le permiten unificar múltiples variables de estado en un solo objeto y consolidar toda la lógica relacionada!

Paso 5: Conecta los controladores de eventos para actualizar el estado

Por último, crea controladores de eventos para establecer las variables de estado. A continuación se muestra el formulario final, con todos los controladores de eventos conectados:

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>¡Correcto!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>Cuestionario sobre ciudades</h2>
      <p>
        ¿En qué ciudad hay un cartel que convierte el aire en agua potable?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Enviar
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Buen intento, pero incorrecto. ¡Inténtalo de nuevo!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Aunque este código es más largo que el ejemplo imperativo original, es mucho menos frágil. Expresar todas las interacciones como cambios de estado te permite introducir nuevos estados visuales sin romper los existentes. También te permite cambiar lo que se debe mostrar en cada estado sin cambiar la lógica de la interacción en sí.

Recapitulación

  • La programación declarativa significa describir la interfaz de usuario para cada estado visual en lugar de microgestionar la interfaz de usuario (imperativa).
  • Cuando desarrolles un componente:
    1. Identifica todos sus estados visuales.
    2. Determina los disparadores humanos y de computadora para los cambios de estado.
    3. Modela el estado con useState.
    4. Elimina el estado no esencial para evitar errores y paradojas.
    5. Conecta los controladores de eventos para actualizar el estado.

Desafío 1 de 3:
Añade y elimina una clase de CSS

Haz que al hacer clic en la imagen elimine la clase CSS background--active del <div> externo, pero agregue la clase picture--active a la <img>. Al hacer clic en el fondo nuevamente, debería restaurar las clases CSS originales.

Visualmente, deberías esperar que al hacer clic en la imagen se elimine el fondo morado y se resalte el borde de la imagen. Al hacer clic fuera de la imagen, se resalta el fondo, pero se elimina el resaltado del borde de la imagen.

export default function Picture() {
  return (
    <div className="background background--active">
      <img
        className="picture"
        alt="Casas de arcoiris en Kampung Pelangi, Indonesia"
        src="https://i.imgur.com/5qwVYb1.jpeg"
      />
    </div>
  );
}