Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:desarrollo_web_react_18:logica_de_componentes

¡Esta es una revisión vieja del documento!


Lógica de componentes

Notas del curso Desarrollo de aplicaciones web con React 18

Como ya has podido observar, React dispone de las herramientas suficientes para comenzar de forma muy sencilla a construir aplicaciones interactivas. Para poder aprovechar su potencial, tenemos que plantear una aplicación que requiera cierta complejidad y separarla en unidades que tengan responsabilidad única: los componentes.

Diseño de la aplicación

Con el objetivo de construir una aplicación incrementando progresivamente la complejidad y la funcionalidad, en este módulo desarrollaremos el prototipo de una sencilla aplicación para gestión de tareas, como primer ejemplo con React.

A continuación tienes el aspecto aproximado de cómo quedará la aplicación una vez que hayas completado todas las lecciones de este módulo:

De momento nos centraremos en los mecanismos que ofrece React para mostrar la información de la que dispongamos. Esto incluye el desarrollo de la interfaz en sí, la combinación de varios componentes y elementos en otros componentes, el paso de datos y la capacidad de modificar, mostrar u ocultar partes de la interfaz en función de los valores de esos datos.

Esto quiere decir que aún no vamos a tener las herramientas para que la aplicación responda a eventos como pulsaciones de botones o cambios de estado de las casillas de verificación, pero vamos a sentar las bases para que añadir ese comportamiento sea un proceso sencillo.

En los archivos asociados al módulo encontrarás una versión de esta aplicación con los siguientes componentes:

  • App
  • ListaTareas
  • Tarea
  • Cabecera

Este último componente se ha dejado como ejercicio práctico, ya que te será muy fácil de completar cuando hayas realizado el aprendizaje de las lecciones. Recuerda implementarlo para comprobar que dominas las técnicas explicadas.

Arquitectura de componentes de React

El objetivo de React es permitir desarrollar una interfaz de aplicación web compleja a partir de componentes sencillos y reutilizables. Estos componentes encapsularán tanto la estructura HTML como los datos que mostrarán.

En la práctica, cada componente consistirá en una función (o clase) que es capaz de proporcionar un nodo del documento que conforma la página web.

Para probar pequeños componentes sin necesidad de construir un proyecto React cada vez, puedes utilizar un servicio de REPL online como CodeSandbox, PlayCode o Repl.it, que permiten escribir código React y ver el resultado en tiempo real.

La función createElement()

Aunque más adelante nos ahorraremos escribirla repetidamente gracias a un poco de azúcar sintáctica, es importante que sepas que la principal funcionalidad encargada de enlazar los elementos HTML en el DOM virtual de React es createElement(). Esta función permite indicar cuál es el elemento principal a crear cuando se construye un componente, incluidos sus atributos o propiedades y sus nodos descendientes. Por eso la vamos a utilizar inicialmente.

La estructura de una llamada a createElement() es la siguiente:

React.createElement(tipo, propiedades, ...descendientes)

donde:

  • tipo puede ser o bien una cadena de caracteres especificando el elemento HTML correspondiente, o bien un componente ya existente
  • propiedades son un objeto que declara el valor de los atributos que queramos asignar
  • descendientes son los nodos hijos del elemento, creados también con la misma función.

Veamos un pequeño ejemplo donde construimos un componente que consiste en un formulario de búsqueda, haciendo uso de createElement():

React.createElement(
  "form", // tipo
  { action: "/search", method: "GET" }, // propiedades
  React.createElement( // hijo 1
    "input",
    { type: "search", placeholder: "Términos de búsqueda" }
  ),
  React.createElement( // hijo 2
    "button",
    { type: "submit" },
    "Buscar"
  )
)

Como puedes observar, el concepto es relativamente similar a la creación de elementos HTML en JavaScript puro, donde contamos con los métodos document.createElement() y appendChild().

Una ventaja de programar algo así con React es que la sintaxis es principalmente declarativa, en lugar de la sintaxis imperativa habitual en JavaScript, lo cual consigue un código más compacto. Además, la función de React también es válida para crear componentes de React, no solo elementos HTML.

Esqueleto básico del componente

A la hora de escribir componentes en React, por lo general comenzaremos siempre con el mismo esqueleto básico: una función que devuelve elementos creados con la función React.createElement(). A continuación puedes observar el mismo formulario de búsqueda que hemos construido anteriormente, convertido en un componente al devolver el elemento de tipo form en una función:

// Nombre del componente;
const FormularioBusqueda = () => {
  // Aquí, opcionalmente, van cálculos previos a la construcción de los elementos
  // se generan los elementos HTML contenidos en el componente:
  return React.createElement(
    "form",
    { action: "/search", method: "GET" },
    React.createElement(
      "input",
      { type: "search", placeholder: "Términos de búsqueda" }
    ),
    React.createElement(
      "button",
      { type: "submit" },
      "Buscar"
    )
  )
}

Una vez que disponemos de esta función FormularioBusqueda, podemos utilizarla de forma idéntica a como si se tratase de otro elemento HTML, en otro componente con React.createElement(FormularioBusqueda). Por ejemplo, podríamos tener un componente Cabecera que tiene un elemento de tipo FormularioBusqueda:

const Cabecera = () => {
  return React.createElement(
    "div",
    null,
    React.createElement("h1", null, "Mi primera app React"),
    React.createElement(FormularioBusqueda)
  )
}

Como puedes observar, la idea es que tratemos a los componentes como si fueran elementos HTML más complejos.

Propiedades

Los componentes pueden recibir algunos parámetros que sirven para mostrar diferente información en función de la instancia del componente que se esté renderizando. Convencionalmente, estos parámetros o propiedades se recogen en un objeto denominado props.

Por ejemplo, supongamos que estamos desarrollando un componente Desplegable que consistirá en un elemento details con su correspondiente summary pero al que más adelante añadiremos estilos y marcado específico. Sería apropiado tener al menos una propiedad para especificar el título del contenido desplegable, y otra para indicar el propio contenido. En ese caso, el componente tendría aproximadamente el siguiente aspecto:

// admite un objeto de propiedades
const Desplegable = (props) => {
  return React.createElement(
    "details", null,
    // utiliza la propiedad titulo
    React.createElement('summary', null, props.titulo),
    // utiliza la propiedad contenido
    React.createElement('p', null, props.contenido)
  )
}

Las propiedades que recibe un componente son de solo lectura, es decir, no deben modificarse en el cuerpo de la función. Se pueden utilizar para calcular otros valores, pero no deben modificarse ya que puede afectar al ciclo de actualizaciones de componentes.

Hay algunos trucos que nos permiten simplificar el código en componentes pequeños. Si el componente no recibe muchas propiedades y el objeto props no es estrictamente necesario, podemos recoger directamente los valores de las propiedades en constantes mediante desestructuración del objeto. Además, si no hay más sentencias que la de retorno, podemos obviar las llaves y la palabra reservada return:

const Desplegable = ({ titulo, contenido }) => (
  React.createElement(
    "details", null,
    React.createElement('summary', null, titulo),
    React.createElement('p', null, contenido)
  )
)

Para proporcionar valores a las propiedades de un componente, lo haremos de la misma forma que utilizamos cuando asignamos atributos a un elemento HTML, con especial cuidado de que coincidan las claves del objeto con los nombres de propiedades que se consultan en el componente:

React.createElement(
  Desplegable,
  { titulo: "Más detalles", contenido: "Contenido detallando el producto" }
)

Lógicamente, si en lugar de contenido hubiésemos llamado, por ejemplo, detalles a ese parámetro, el párrafo p que se crea en el componente Desplegable no contendría ningún texto. Aunque ahora lo veas claro porque estamos escribiendo el objeto de propiedades “tal cual”, más adelante nos ayudaremos de una sintaxis más simple y habrá que prestar atención a este detalle.

La captura de pantalla que puedes ver a continuación contiene el prototipo de una aplicación React utilizando los componentes que acabamos de desarrollar:

Propiedades especiales

Hay algunos nombres de propiedades que están reservados para casos especiales. Por un lado, tenemos children que siempre es accesible desde dentro del componente y, por otro, tenemos key y ref que no son accesibles ya que sirven para gestionar el comportamiento de React:

  • children: esta propiedad permite acceder a los nodos hijo del componente que estamos implementando, si los tuviera. Por ejemplo, podríamos tener un componente Modal representando una ventana de la interfaz que pueda albergar cualquier contenido, que se incluiría como nodos hijo y se accedería en esta propiedad. Más adelante veremos otros usos útiles de esta funcionalidad.
  • key: esta propiedad sirve para diferenciar elementos similares que se hayan creado, por ejemplo, en un bucle.
  • ref: en ocasiones, en la lógica de un componente necesitamos una referencia a un elemento HTML o a otro componente para poder realizar cambios sobre él. Por ejemplo, pausar o reproducir un vídeo, o poner el foco en un campo de entrada. La propiedad ref se coloca en un elemento para disponer de esta referencia.

La sintaxis JSX

Hemos visto que un componente se encarga de renderizar una parte de una página web y lo hace de forma que es reutilizable. Sin embargo, si nuestra intención es componer toda la interfaz en base a estos componentes, puede ser tedioso estar escribiendo tanto código JavaScript, especialmente tantas llamadas a la función React.createElement().

Para solucionar este obstáculo, se introdujo una sintaxis que facilita el desarrollo de componentes al asimilarse lo máximo posible a HTML, a la vez que permite introducir las variables y los valores que necesitemos integrar en la interfaz. Esta sintaxis se denomina JSX, y no es HTML válido ni JavaScript válido, por lo que se requiere una biblioteca de traducción que la convierta en JavaScript estándar.

Escribiendo componentes HTML en JSX

Vamos a ver un ejemplo de cómo podemos escribir un componente sencillo utilizando JSX. Para ello, recuperamos uno de los ejemplos que desarrollamos previamente:

const FormularioBusqueda = () => {
  return React.createElement(
    "form",
    { action: "/search", method: "GET" },
    React.createElement(
      "input",
      { type: "search", placeholder: "Términos de búsqueda" }
    ),
    React.createElement(
      "button",
      { type: "submit" },
      "Buscar"
    )
  )
}

La traducción de JSX a JavaScript es gracias a Babel, que ya viene instalado y configurado en Vite.

Para convertir a JSX, simplemente lo escribimos como si se tratase de HTML, envuelto entre paréntesis para evitar problemas con los saltos de línea:

const FormularioBusqueda = () => {
  return (
    <form action="search" method="GET">
        <input type="search" placeholder="Términos de búsqueda" />
        <button type="submit">Buscar</button>
    </form>
  )
}

Puedes comprobar cómo Babel traduce JSX a código JavaScript en el intérprete online de Babel, que es el que estamos usando en la imagen inferior. Como verás, el resultado es muy similar al que tenemos escrito más arriba.

Uso de JavaScript en JSX

Al usar JSX no se añade ninguna limitación con respecto a cuando usamos puro JavaScript, y en consecuencia, tenemos también una sintaxis para insertar valores de variables y los resultados de evaluar código JavaScript dentro del propio marcado estilo HTML. Esta consiste en usar llaves para indicar el código que hay que interpretar. Por ejemplo, considera el siguiente componente donde se saluda a la persona que ha iniciado sesión en nuestra página web:

const Saludo = ({ nombreUsuario }) => {
    const mensaje = `¡Hola ${nombreUsuario}!`
    return React.createElement("div", null, mensaje)
}

El elemento div podemos traducirlo de forma sencilla a JSX, y para que el nodo de texto sea el mensaje, simplemente lo envolveremos en llaves como sigue:

const Saludo = ({ nombreUsuario }) => {
    const mensaje = `¡Hola ${nombreUsuario}!`
    return <div>{mensaje}</div>
}

Observa que, en este caso, no ha sido necesario indicar los paréntesis que envuelven al elemento devuelto, puesto que el marcado comienza en la misma línea que la palabra reservada return.

Una alternativa que lo simplificaría aún más sería calcular el mensaje directamente en la parte de JSX, eliminando así la necesidad de llaves para el cuerpo de la función:

const Saludo = ({ nombre }) => <div>{`¡Hola ${nombre}!`}</div>

Como ves, con estas técnicas podemos incluso desarrollar pequeños componentes que ocupen una o pocas líneas de código. Esto es especialmente útil cuando queremos componer elementos sencillos reutilizables como botones personalizados o envolturas para otros componentes.

Todas las etiquetas deben ir cerradas. Aunque en HTML5 se pueden usar etiquetas sin cerrar (como <br> o <input>), en el caso de JSX hay que cerrarlas para evitar errores de sintaxis: <br /> o <br></br>.

Uso de componentes en JSX

La sintaxis JSX no solo sirve para especificar elementos HTML en la interfaz de nuestro componente, sino también otros componentes de forma similar a como se hace con React.createElement(). Por ejemplo, integrar un componente Saludo en otro tipo Cabecera sería tan sencillo como tratarlo como un elemento más:

const Cabecera = () => (
    <div>
        <h1>Mi primera app React</h1>
        <FormularioBusqueda />
        <Saludo nombre="Ana" />
    </div>
}

La propiedad nombre con el valor “Ana” la hemos proporcionado como si se tratase de un atributo HTML. También es posible utilizar la sintaxis de llaves para calcular el valor de una de estas propiedades:

const Cabecera = ({ nombrePila, apellido }) => (
    <div>
        <h1>Mi primera app React</h1>
        <FormularioBusqueda />
        <Saludo nombre={`${nombrePila} ${apellido}`} />
    </div>
}

Al usar la sintaxis JSX no es necesario importar createElement en nuestro código, es decir, podemos quitar la línea import { createElement } from 'react'. Los imports que sean necesarios, los hará Babel por nosotros cuando compile el código JSX a JavaScript.

Restricciones y convenciones sintácticas

A la hora de escribir componentes en React, nos encontraremos con algunas limitaciones que presenta esta biblioteca por diversos motivos y otras convenciones de uso. Es importante que las conozcas y las tengas en cuenta para evitar que obstaculicen el proceso de desarrollo; ya que, en caso contrario, es probable que te tropieces con ellas constantemente.

En esta lección veremos una lista de estas circunstancias que hay que considerar y qué efectos tienen en el código que escribimos.

Un componente solo puede devolver un elemento de primer nivel

Una de las restricciones más básicas que se nos plantean al desarrollar componentes es que deben estar formados por un solo elemento de primer nivel, que a su vez pueden contener más elementos como hijos. Es decir, no podríamos escribir un componente como el siguiente:

// Error al devolver dos elementos de primer nivel
const Formulario = () => (
    <input type="email" placeholder="ejemplo@example.org">
    <input type="submit" value="Iniciar sesión">
)

Para arreglarlo, únicamente sería necesario envolver ambos elementos en uno que los contenga:

// Error al devolver dos elementos de primer nivel
const Formulario = () => (
    <form>
        <input type="email" placeholder="ejemplo@example.org">
        <input type="submit" value="Iniciar sesión">
    </form>
)

Es posible que no nos interese incorporar este elemento único de primer nivel al componente, “ensuciando” el DOM de nodos tipo <div> o <span> que no jueguan rol alguno en la interfaz de nuestra web. Existe un atajo para solventar este problema, denominado React.Fragment. Un Fragment es un tipo de componente especial propio de React que no genera ningún marcado en el DOM final. Dicho de otro modo: es un nodo fantasma que no se traduce en ningún elemento en la página web.

Hay dos formas de utilizarlo:

  1. Importar React.Fragment y utilizarlo como un componente más (<React.Fragment>{ elementos }</React.Fragment>)
  2. Si tenemos Babel ya configurado (como en nuestro proyecto Vite), es utilizar marcas HTML aparentemente vacías <>...</>, de la siguiente manera:
// Error al devolver dos elementos de primer nivel
const Formulario = () => (
    <>
        <input type="email" placeholder="ejemplo@example.org"/>
        <input type="submit" value="Iniciar sesión"/>
    </>
)

Babel entenderá la sintaxis y la traducirá a un React.Fragment en el código final.

Las etiquetas siempre deben cerrarse

A diferencia de HTML5, que soporta algunas etiquetas sin cierre como <img>, <input> o <p>, en sintaxis JSX todas las etiquetas deben tener su correspondiente etiqueta de cierre, como en <Boton>Púlsame</Boton>, o bien estar auto-cerradas, por ejemplo <Formulario />.

En cualquier caso, el compilador de JSX proporciona un mensaje de error bastante informativo cuando nos dejamos una etiqueta sin cerrar. Puedes comprobarlo eliminando la etiqueta de cierre o la barra inclinada en algún elemento HTML o componente, y verás un mensaje similar al siguiente:

Expected corresponding JSX closing tag for <Tarea>. (61:12)

Convenciones de nombres

Es habitual que los nombres de los componentes comiencen por mayúscula y utilicen Pascal-case para separar palabras. El resto de las funciones, las variables y las constantes, por lo general, se escriben con la primera letra en minúscula, con notación camel-case. Por tanto, nuestros componentes se llamarán BotonPrimario o FormularioBusqueda, mientras que las variables serán estaActivo, contenido, etc.

Existen ciertas categorías de funciones que son de uso muy común en React y, por tanto, siguen convenciones para sus nombres también. Aunque los estudiaremos a fondo más adelante, debes saber que un hook se reconoce porque su nombre comienza por use, por ejemplo: useState, useEffect, useContext. Asimismo, un reducer suele acabar con la palabra Reducer, por ejemplo: rootReducer, listaCompraReducer.

Además, la mayoría de atributos HTML que se escriben normalmente en minúscula (o con guion) pasan a escribirse en camel-case. Por poner algunos ejemplos: onClick, autoFocus.

La excepción a esta regla son los atributos de accesibilidad, que comienzan por aria- como aria-label o aria-required que se escriben igual que en HTML.

Atributos HTML que cambian de nombre

Hay algunos atributos que no se pueden utilizar directamente en JSX y debemos sustituirlos por el nombre apropiado:

  • className en lugar de class. Puesto que class es palabra reservada en JavaScript, no la podemos usar para indicar un atributo de clase de un elemento HTML. En su lugar utilizaremos className, por ejemplo: <div className="bg-white"></div>.
  • Los atributos relacionados con eventos utilizan camel-case. Por ejemplo, no escribimos <input onclick={reaccion} /> sino <input onClick={reaccion} />.
  • defaultChecked en lugar de checked. El atributo checked existe pero no determina el valor inicial del estado (marcada o no marcada) de una casilla, sino el actual. Por tanto, si lo fijamos será imposible que el usuario desmarque o marque la casilla a voluntad, ya que no podrá cambiar de estado. El atributo defaultChecked sí que indica el estado inicial.
  • htmlFor en lugar de for, ya que for es una palabra reservada en JavaScript y nos encontramos con el mismo obstáculo que con class.

Ausencia de efectos secundarios en componentes

Los componentes creados como funciones, deben evitar a toda costa presentar efectos secundarios, esto es: modificar variables globales, alterar el estado de otros componentes u otros elementos de la página web, generar números aleatorios, etc.

Esto se debe a que React espera que la función de renderizado de un componente se pueda ejecutar repetidamente con los mismos parámetros sin que eso altere el resultado en cuanto a la interfaz que genere.

Por tanto, debemos cuidar que nuestro código no genere efectos inesperados. En tal caso, puede que React lo detecte y nos informe con un warning o un error, o bien pase indetectado y cause comportamientos impredecibles.

Más adelante veremos cómo aislar los efectos secundarios que sean necesarios en estos componentes, con el hook useEffect.

El componente raíz de la aplicación

Cada aplicación basada en React tendrá un componente raíz del que derivarán los demás, dado que la estructura de la aplicación tiene forma de árbol, al igual que la estructura de una página HTML.

Al crear un nuevo proyecto con npx create-vite, dispondremos de un componente raíz denominado App dentro del cual implementaremos la interfaz principal.

Modo estricto del intérprete

Además, si te fijas en el fichero src/main.jsx notarás que hay un componente más arriba en la estructura, que se llama React.StrictMode. Este componente activa una serie de comprobaciones de seguridad y de potenciales problemas, que nos avisarán si utilizamos algunas características obsoletas, no seguras o programamos algún componente de forma incorrecta. Actualmente, con StrictMode podremos saber si:

  • Algún componente presenta ciclos de vida inseguros para el renderizado concurrente,
  • Usamos las referencias basadas en cadenas de caracteres, que están obsoletas
  • Alguna de las funciones que deben ser puras tienen efectos secundarios
  • Se está usando una API antigua para definir contextos
  • Utilizamos findDOMNode, que está obsoleto

Es conveniente dejar ese componente activo durante el desarrollo de la aplicación, pero podremos eliminarlo al configurarla para producción, ya que añade una sobrecarga innecesaria una vez que la aplicación está terminada.

Modificando el nodo raíz

Volviendo al nodo App, este se puede invocar porque está importado en las primeras líneas del archivo, concretamente, en la que indica:

import App from './App'

Esto quiere decir que su implementación está en el archivo App.jsx del mismo directorio. Si ahora nos dirigimos a ese archivo, veremos el contenido del componente implementado en forma de función:

function App() {
  const [count, setCount] = useState(0)
 
  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src="{reactLogo} className="logo react" alt="React logo" /"> 
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.jsx

and save to test HMR

      </p>
    </div>
    <p className="read-the-docs">
      Click on the Vite and React logos to learn more
    </p>
  </div>
)

} </code>

Para comenzar a crear nuestra aplicación, eliminaremos todo el código no esencial de esta plantilla, quedándonos con lo siguiente:

function App() {
  return (
    <div className="App">
    </div>
  )
}

Antes de empezar a realizar modificaciones, siempre conviene tener lanzado el servicio web con el comando npm run dev y la página web abierta en un navegador, de forma que veamos cualquier cambio o error de forma instantánea.

También podemos eliminar los imports que ya no son necesarios, como el de useState y el de reactLogo. Ahora tendremos un lienzo en blanco para comenzar a construir nuestra aplicación.

Asociando el componente raíz a un elemento del DOM

En el archivo src/main.jsx se llama al método ReactDOM.createRoot() que asocia un elemento del DOM con el nodo raíz de la aplicación React. En este caso, el elemento de la web asociado será el que tenga id="root".

En el caso de que desarrollemos una aplicación sin usar el proyecto por defecto de Vite, tendremos que asociar de forma análoga un elemento de documento HTML original al componente raíz.

Prototipo de la estructura

Aunque el proyecto que vamos a desarrollar a lo largo del curso es una aplicación de gestión de tableros tipo Kanban, por ahora vamos a limitarnos a lo esencial y vamos a crear una lista de tareas. El primer paso es escribir un prototipo de cómo podría quedar la lista de tareas con varias tareas ya incluidas. Por ejemplo, podríamos comenzar con una lista HTML con cada tarea pendiente en un ítem de la lista:

function App() {
  return (
    <div className="App">
        <h1>Kanpus</h1>
        <p>Tareas pendientes:</p>
        <ul>
            <li>Aprender componentes React</li>
            <li>Completar las prácticas del módulo 1</li>
            <li>Realizar la autoevaluación</li>
        </ul>
    </div>
  )
}

En principio, con estos elementos no nos quedará aún una interfaz muy estética ni muy interactiva, pero nos servirá para identificar un elemento que se repite y que, de añadir complejidad a la aplicación, se debería separar para facilitar la reutilización del código. Por ejemplo, si añadiéramos un checkbox para indicar tareas completadas y un botón para eliminarlas:

<li>
  <input type="checkbox" defaultChecked={false} />
  Aprender componentes React
  <button>Eliminar</button>
</li>

En las próximas lecciones desarrollaremos varios componentes que irán dando forma y añadiendo funcionalidad a nuestra lista de tareas, que próximamente se convertirá en una aplicación de tableros Kanban.

Puedes comprobar que el estilo que se usa en la aplicación web proviene de un archivo CSS llamado App.css que se importa al principio de App.jsx. Modifica este archivo de estilo para adaptar a tu gusto la interfaz de la lista de tareas.

Creando y componiendo componentes

A lo largo de esta lección aprenderás a construir nuevos componentes y a incluirlos para que formen parte de la estructura de otros.

Componente Tarea

Dado que cada elemento de la lista de tareas va a replicar la misma estructura básica, con una casilla para marcar como completada y un botón para eliminar, vamos a crear una nueva función que nos va a definir una tarea genérica. A este nuevo componente lo podemos llamar, simplemente, Tarea. En principio, podemos utilizar el mismo código que teníamos para cualquiera de las tareas:

const Tarea = () => (
  <li>
    <input type="checkbox" defaultChecked={false} />
    Aprender componentes React
    <button>Eliminar</button>
  </li>
)

Fíjate en que estamos utilizando la sintaxis flecha para declarar la función y que, como no tenemos que realizar ningún cómputo sino simplemente devolver el contenido del componente, podemos omitir el uso de llaves y la palabra reservada return. Además, el atributo defaultChecked establece el estado inicial de la casilla como desmarcada.

Ahora que tenemos el componente Tarea listo para reutilizar, nuestro componente App podría quedar como sigue:

<div className="App">
  <h1>Kanpus</h1>
  <p>Tareas pendientes:</p>
  <ul>
    <Tarea />
    <Tarea />
    <Tarea />
  </ul>
</div>

Simplemente hemos repetido el nodo referente al componente Tarea tres veces para disponer de tres elementos en la lista. Sin embargo, esto nos genera tareas idénticas con el mismo título. A continuación arreglaremos esto utilizando propiedades.

Uso de propiedades en el componente

Para que cada tarea pueda tener un contenido diferente, hemos de proporcionarle una propiedad que contenga el título correspondiente. En el componente podemos recibir el parámetro props (recuerda que la convención es usar este nombre para las propiedades recibidas) y acceder al título:

const Tarea = (props) => (
  <li>
    <input type="checkbox" defaultChecked={false} />
    {props.titulo}
    <button>Eliminar</button>
  </li>
)

Ahora en el componente principal basta con indicar un atributo titulo diferente para cada tarea:

<Tarea titulo="Aprender componentes de React" />
<Tarea titulo="Completar las prácticas del módulo 1" />
<Tarea titulo="Realizar la autoevaluación" />

Reutilización de componentes

Ahora que tenemos un componente Tarea que encapsula las propiedades e interfaz de un elemento de nuestra lista de tareas, vamos a extraer los datos de la parte de interfaz del componente App para simplificar su implementación y hacerla totalmente independiente de la cantidad y contenido de tareas que existan.

Separando datos e interfaz

Puesto que la aplicación que estamos desarrollando está aún en fase de prototipado, no nos hemos preocupado de tener un almacén para la información a mostrar. Lógicamente, no tendría cabida que datos como los títulos de las tareas hubiese que escribirlos a mano en la interfaz web. De la misma forma, no tiene sentido que el número de tareas disponibles esté determinado de antemano.

Para separar la información de la parte del componente que simplemente ha de mostrarla, vamos a construirnos un array con la lista de títulos de ejemplo que tenemos:

function App() {
  const tareas = [
    "Aprender componentes de React",
    "Completar las prácticas del módulo 1",
    "Realizar la autoevaluación"
  ]
 
  // resto del componente...

Una vez que tenemos los títulos almacenados en una estructura de datos, podemos simplificar el marcado recorriéndola con el método map, que devolverá un array de nodos para incluir en el componente:

<ul>
    {tareas.map(tarea => <Tarea titulo={tarea} />)}
</ul>

Observa que hemos de enmarcar la sentencia entre llaves para que se interprete como código JavaScript del que queremos recoger un resultado.

En este caso, para cada título en el array tareas se construirá un nodo del componente Tarea con el título correspondiente aplicado. Aunque el método map técnicamente devuelve un array y no un nodo JSX, es también un resultado válido: simplemente se colocan los elementos en el orden en que estuvieran en el array original.

Identificador único key

Si consultamos la consola de las herramientas de desarrollo de nuestro navegador, es muy probable que aparezca un mensaje de React avisando de que es necesario indicar un identificador en un atributo key a cada elemento que generemos mediante un bucle como el anterior.

React debe tener un identificador único para cada elemento para llevar un registro de los cambios que sufra y poder actualizar el nodo correspondiente en el DOM. Al crear elementos iterativamente, es importante identificarlos bien por si se crean en un orden distinto en otra ocasión en que se renderice el componente.

Por lo general, el identificador provendrá de los datos almacenados en el servidor y simplemente lo asociaremos como valor a la propiedad key. Por ahora, nos basta con utilizar el índice del bucle para que cada tarea esté identificada de forma única y evitemos el aviso de React:

<ul>
    {tareas.map((tarea, i) => <Tarea titulo={tarea} key={i} />)}
</ul>

Renderizado condicional

Una práctica muy habitual en interfaces de aplicaciones web es que tengan la capacidad de mostrar u ocultar elementos en función del estado en cada momento o de diversos eventos. En React, esta funcionalidad se conoce como renderizado condicional y es muy sencilla de conseguir, como veremos a continuación.

Preparación previa

Una funcionalidad básica de una lista de tareas consiste en poder tener tareas pendientes y tareas completadas. Vamos a introducir una nueva propiedad en el componente Tarea denominada completada. Para ello, simplemente hemos de añadirla al almacén de datos de ejemplo y proporcionarla al crear el componente (o sea, usaremos objetos en lugar de simples cadenas), quedando el componente App como sigue:

function App() {
  const tareas = [
    {
      titulo: "Aprender componentes de React",
      completada: false
    },
    {
      titulo: "Completar las prácticas del módulo 1",
      completada: true
    },
    {
      titulo: "Realizar la autoevaluación",
      completada: false
    }
  ]
 
  return (
    <div className="App">
      <h1>Kanpus</h1>
      <p>Tareas pendientes:</p>
      <ul>
        {tareas.map(tarea => <Tarea titulo={tarea.titulo} completada={tarea.completada} />)}
      </ul>
    </div>
  )
}

La sintaxis JSX soporta el paso de atributos mediante desestructuración de objetos, por lo que, teniendo el objeto correspondiente en la variable tarea, también podríamos indicar las propiedades de Tarea de esta forma:

    <Tarea {...tarea}>

Este truco nos puede ser muy útil cuando necesitemos pasar una cantidad grande de propiedades de un componente a otro. En este caso, siendo sólo 2 propiedades y a efectos didácticos, nos quedamos con la forma más explícita.

La única modificación necesaria sobre el estado actual del componente Tarea es sustituir el valor del atributo defaultChecked para que la casilla esté marcada cuando una tarea esté completada:

const Tarea = (props) => {
  return (
    <li>
        <input type="checkbox" defaultChecked={props.completada} />
        {props.titulo}
        <button>Eliminar</button>
    </li>
  )
}

Puesto que vamos a utilizar repetidamente esta propiedad, conviene extraerla mediante desestructuración del objeto de propiedades:

const Tarea = ({titulo, completada}) => {
  return (
    <li>
        <input type="checkbox" defaultChecked={completada} />
        {titulo}
        <button>Eliminar</button>
    </li>
  )
}

Mostrando elementos

Supongamos que una tarea solo se pueda eliminar una vez que se marca como completada. Lo razonable sería que el botón de “Eliminar” apareciese únicamente para las tareas completadas, claro. No es difícil conseguirlo, ya que podemos aprovechar el operador && de JavaScript dentro de la sintaxis JSX para construir una expresión que evalúe a un nodo en caso positivo y a false en caso negativo. Tenemos la ventaja de que el valor false no renderiza ningún elemento, por lo que basta con la siguiente expresión:

{completada &&
    <button>Eliminar</button>
}

De esta forma, únicamente mostramos el botón cuando es posible eliminar la tarea.

Por si lo anterior no te ha quedado claro, estamos utilizando el operador && como un operador de cortocircuito, que nos evita escribir un condicional entero. En este caso, el operador && devuelve el primer operando si éste es false y, en caso contrario, devuelve el segundo operando. Por tanto, si completada es false, la expresión completa evalúa a false y no se renderiza ningún elemento. Si completada es true, la expresión completa evalúa a <button>Eliminar</button> y se renderiza el botón.

Ocultando elementos

Se puede aplicar una estrategia similar a la anterior cuando queremos quitar un nodo del DOM al cumplirse una condición. Esta vez, en lugar del operador && utilizaremos el ||, que devuelve true si el primer operando es verdadero y, en caso contrario, devuelve el segundo operando.

Es el mismo concepto de cortocircuito de expresiones lógicas que hemos visto en el apartado anterior, pero con el operador || en lugar del &&.

Entonces, si queremos disponer de un botón de edición para las tareas que no estén aún completadas, utilizaremos el siguiente código:

{completada ||
    <button>Editar</button>
}

Como ves en la siguiente imagen, los botones de eliminar aparecen únicamente en las tareas completadas y los de editar aparecen salvo cuando la tarea en cuestión está completada.

Alternando elementos

Si aplicamos la misma filosofía que hemos utilizado previamente con los operadores && y || al operador ternario (?:), obtendremos expresiones que son capaces de seleccionar uno u otro elemento en función del valor de verdad de la condición. Por ejemplo, podríamos mejorar el aspecto visual de la lista de tareas añadiendo un indicador “TODO” en las tareas pendientes y “DONE” en las tareas completadas.

Para ello, envolveremos la casilla en una etiqueta <label> cuyo nodo de texto dependerá de si la tarea ha sido completada:

<label>
    <input type="checkbox" defaultChecked={completada} />
    {completada ? "DONE" : "TODO"}
</label>

Seleccionando atributos

La mecánica que hemos desarrollado para mostrar u ocultar elementos y texto también es perfectamente aplicable a propiedades y atributos. Esto nos permite alterar la forma en que se renderizan los elementos y los componentes según se cumplan ciertas condiciones. Por ejemplo, tenemos la opción de aplicar una clase diferente a las tareas pendientes y a las completadas:

<li className={completada ? "done" : "todo"}>

Incorporando algo de estilo al fichero App.css que se importa en el script de los componentes, tendremos algo como lo siguiente:

Componentes condicionalmente vacíos

Al igual que en sintaxis JSX no se convierten los valores booleanos, true y false, en ningún elemento nuevo, tampoco se convierte en elemento el valor null. Esto da lugar a que se puedan desarrollar componentes que solo se muestran bajo condiciones concretas.

A continuación vamos a extraer del componente App la parte de la interfaz que se dedica únicamente a mostrar la lista de tareas en sí. Dentro de este nuevo componente ListaTareas, devolveremos null en el caso de que la lista de tareas esté vacía. Esto nos evitará el caso en que rendericemos una caja vacía en la página cuando no haya ninguna tarea disponible:

const ListaTareas = ({ tareas }) => {
  if (tareas.length == 0) {
    return null;
  }
 
  return (
    <ul>
      {tareas.map(tarea => <Tarea {...tarea} />)}
    </ul>
  )
}

Prácticas propuestas para el módulo

informatica/programacion/cursos/desarrollo_web_react_18/logica_de_componentes.1709204904.txt.gz · Última modificación: por tempwin