Creando componentes en nuestra app de React
En este punto, nuestra aplicación es un monolito. Antes de hacer que haga cosas, necesitamos dividirlo en componentes manejables y descriptivos. React no tiene ninguna regla estricta sobre lo que es y no es un componente - ¡eso depende de ti! En este artículo, le mostraremos una forma sensata de dividir nuestra aplicación en componentes.
Prerequesitos: |
Estar familiarizado con los lenguajes básicos HTML, CSS, y JavaScript, conocimientos de la terminal/linea de comandos. |
---|---|
Objetivo: | Mostrar una forma sensata de dividir nuestra aplicación _Todo list_ (lista de tareas) en componentes. |
Definiendo nuestro primer componente
Definir un componente puede parecer complicado hasta que tenga algo de practica, pero el principio es:
- Si este representa un "fragmento" obvio de tu aplicación, probablemente sea un componente.
- Si se reutiliza frecuentemente, probablemente sea un componente.
El segundo punto es especialmente valioso: crear un componente a partir de los elementos comunes de la UI permite que cambies tu código en un solo lugar y ver esos cambios en todos los lugares donde se usa ese componente. Tampoco es necesario dividir todo en componentes. Tomemos el segundo punto como inspiración y hagamos un componente a partir de la parte más importante y más reutilizada de la interfaz de usuario: un elemento de la lista de tareas pendientes.
Haz un <Todo />
Antes de que podamos crear un componente, debemos crear un archivo nuevo para él. De echo, debemos crear un directorio para nuestros componentes. Los siguientes comandos crean un directorio components
y luego, dentro de este, un archivo llamado Todo.js
. Asegurate de estar en la raíz de tu aplicación antes de ejecutarlas.
mkdir src/components
touch src/components/Todo.js
Nuestro archivo nuevo Todo.js
esta vació actualmente! Ábrelo y agrega la primera linea:
import React from "react";
Ya que vamos a crear un componente llamado Todo
, también puede comenzar a agregar el código para eso en Todo.js
, de la siguiente manera. En este código, definimos la función y la exportamos en la misma línea:
export default function Todo() {
return (
// …
);
}
Esto esta bien hasta ahora, ¡pero nuestro componente debe devolver algo!, Vamos de vuelta a src/App.js
, copie el primer <li>
de la lista desordenada y péguelo en Todo.js
para que se lea así:
export default function Todo() {
return (
<li className="todo stack-small">
<div className="c-cb">
<input id="todo-0" type="checkbox" defaultChecked={true} />
<label className="todo-label" htmlFor="todo-0">
Eat
</label>
</div>
<div className="btn-group">
<button type="button" className="btn">
Edit <span className="visually-hidden">Eat</span>
</button>
<button type="button" className="btn btn__danger">
Delete <span className="visually-hidden">Eat</span>
</button>
</div>
</li>
);
}
Nota: Los componentes siempre deben devolver algo. Si en cualquier punto en el futuro intentas renderizar un componente que no devuelve nada, React mostrará un error en el navegador.
Nuestro componente Todo
esta completo, por ahora; ahora podemos usarlo. En App.js
, agrega la siguiente linea hasta arriba del archivo para importar Todo
;
import Todo from "./components/Todo";
Con este componente importado, podemos reemplazar todos los elementos li
en App.js
por el componente <Todo />
, Tu ul
debería leerse así:
<ul
role="list"
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading">
<Todo />
<Todo />
<Todo />
</ul>
Cuando vuelves a mirar tu navegador, notarás algo desafortunado: ¡tu lista ahora repite la primera tarea 3 veces!
No queremos comer unicamente; tenemos otras cosas que - bueno - que hacer. A continuación veremos cómo podemos hacer que diferentes llamadas de componentes genere contenido único.
Haz un <Todo />
único
Los componentes son poderosos porque nos permiten reutilizar partes de nuestra UI, y hacer referencia a un lugar para el origen de nuestra UI. El problema es que normalmente no queremos reutilizar todos los componentes, queremos reutilizar la mayoría, y cambiar piezas pequeñas. Aquí es donde llegan los props (propiedades).
¿Que hay en un name
?
Para rastrear los nombres de las tareas que queremos completar, debemos asegurarnos de que cada componente <Todo />
renderize un nombre único.
En App.js
asigna una propiedad name
a cada <Todo />
. Vamos a usar los nombres de nuestras tareas que teníamos anteriormente:
<Todo name="Eat" />
<Todo name="Sleep" />
<Todo name="Repeat" />
Cuando actualice su navegador, verá... exactamente lo mismo que antes. Le agregamos algunas props a nuestro <Todo />
, pero no lo estamos usando todavía. Vamos de vuelta a Todo.js
y solucionemos eso.
Primero modifique la definición de su función Todo()
para que tome props
como parámetro. Puede imprimir los props
en un console.log()
, si desea comprobar que el componente los recibe correctamente.
Una vez que este seguro que su componente esta obteniendo los props
, puede reemplazar todas las ocurrencias de Eat
por el prop name
. Recuerde: cuando está en medio de una expresión JSX, use llaves para inyectar el valor de una variable.
Poniendo todo eso junto, su función Todo()
debería quedar así:
export default function Todo(props) {
return (
<li className="todo stack-small">
<div className="c-cb">
<input id="todo-0" type="checkbox" defaultChecked={true} />
<label className="todo-label" htmlFor="todo-0">
{props.name}
</label>
</div>
<div className="btn-group">
<button type="button" className="btn">
Edit <span className="visually-hidden">{props.name}</span>
</button>
<button type="button" className="btn btn__danger">
Delete <span className="visually-hidden">{props.name}</span>
</button>
</div>
</li>
);
}
Ahora su navegador debería mostrar tres tareas únicas. Sin embargo, queda otro problema: todos siguen marcados de forma predeterminada.
¿Está completado
?
En nuestra lista estática original, únicamente Eat
estaba marcado. Una vez más, queremos reutilizar la mayoría de la UI que constituye el componente <Todo />
, pero cambia una cosa. ¡Eso es un buen trabajo para otra prop!.
Agrega una nueva prop a cada <Todo />
en App.js
llamado completed
. El primero (Eat
) debería tener el valor true
; el resto debería ser false
:
<Todo name="Eat" completed={true} />
<Todo name="Sleep" completed={false} />
<Todo name="Repeat" completed={false} />
Para usar estos props, debemos volver a Todo.js
. Cambia el atributo defaultChecked
en el <input />
para que su valor sea igual a la prop completed
. Una vez terminado, el elemento <input />
del componente Todo
se verá así:
<input id="todo-0" type="checkbox" defaultChecked={props.completed} />
Y su navegador debería actualizarse para mostrar que solo Eat
está marcado:
Si cambia el prop completed
en cada componente <Todo />
, su navegador marcará o desmarcará los checkbox equivalentes respectivamente.
Asigna algún id
, porfavor
Ahora, nuestro componente <Todo />
asigna un atributo id
con el valor todo-0
a cada tarea. Esto es una mala practica en HTML porque los atributos id
deben ser únicos (son utilizados como un identificador único para fragmentos de documentos, por CSS, JavaScript, etc.). Esto significa que debemos darle a nuestro componente un id
que tome un valor único para cada Todo
Para seguir con el mismo patron que teníamos inicialmente, vamos a darle a cada instancia del componente <Todo />
un ID con el formato todo-i
, donde i
cada vez es mas grande por uno:
<Todo name="Eat" completed={true} id="todo-0" />
<Todo name="Sleep" completed={false} id="todo-1" />
<Todo name="Repeat" completed={false} id="todo-2" />
Ahora vuelve a Todo.js
y usa la prop id
. Es necesario reemplazar el valor del atributo id
del elemento <input />
, así como el valor del atributo htmlFor
de su etiqueta:
<div className="c-cb">
<input id={props.id} type="checkbox" defaultChecked={props.completed} />
<label className="todo-label" htmlFor={props.id}>
{props.name}
</label>
</div>
Hasta ahora, ¿Todo bien?
Estamos haciendo un bueno uso de React, ¡pero podemos hacerlo mejor! nuestro código es repetitivo. Las tres lineas que renderiza nuestro componente <Todo />
son muy idénticos, con una sola diferencia: el valor de cada prop.
Podemos limpiar nuestro código con uno de las capacidades principales de JavaScript: La iteración. Para usar la iteración, primero debemos repensar nuestras tasks.
Tareas como datos
Actualmente cada una de nuestras tareas contiene tres piezas de información: su nombre, si se ha completado, y su ID único. Estos datos se traducen muy bien en un objeto. Ya que tenemos mas de una tarea, un arreglo de objetos funcionaría muy bien para representar estos datos.
En src/index.js
, crea una nueva const
debajo de la última importación, pero antes de ReactDOM.render()
:
const DATA = [
{ id: "todo-0", name: "Eat", completed: true },
{ id: "todo-1", name: "Sleep", completed: false },
{ id: "todo-2", name: "Repeat", completed: false },
];
A continuación pasaremos DATA
a <App />
como una prop, llamado tasks
, quedando de la siguiente manera nuestro código:
ReactDOM.render(<App tasks={DATA} />, document.getElementById("root"));
Este arreglo ahora esta disponible en el componente App
como props.tasks
. Puede usar console.log
para comprobarlo, si lo desea.
Nota: TODAS_MAYUSCULAS
los nombres de las constantes no tienen un significado especial en Javascript; es una convención para decirle a otros desarrolladores "Estos datos nunca cambiarán después de haberse definido aquí".
Renderizado con iteración
Para renderizar nuestro arreglo de objetos, tenemos que convertir cada uno en un componente <Todo />
. JavaScript nos brinda un método para transformar los datos de un arreglo en algo: Array.prototype.map()
.
Antes de la sentencia return de App()
, crea una nueva const
llamada taskList
y use map()
para transformarla. Comencemos convirtiendo nuestra arreglo de tasks
en algo simple: el name
de cada tarea:
const taskList = props.tasks?.map((task) => task.name);
Intentemos reemplazar todos los hijos de <ul>
con taskList
:
<ul
role="list"
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading">
{taskList}
</ul>
Este nos ayuda a mostrar nuevamente todos los componentes, pero tenemos mas trabajo que hacer: el navegador representa el nombre de cada tarea como texto no estructurado.
Falta la estructura de nuestro HTML - ¡la etiqueta <li>
y sus casillas de verificación y botones!.
Para corregirlo, necesitamos devolver un componente <Todo />
de nuestra función map()
- ¡recuerda que JSX permite mezclar JavaScript y estructura de marcado! Probemos lo siguiente reemplazando lo que ya tenemos.
const taskList = props.tasks.map((task) => <Todo />);
Mire nuevamente su aplicación; ahora nuestras tareas se parecen más a las de antes, pero les faltan los nombres de las propias tareas. Recuerde que cada tarea que mapeamos tiene las propiedades id
, name
y completed
que queremos pasar a nuestro componente<Todo />
. Si juntamos ese conocimiento, obtenemos un código como este:
const taskList = props.tasks.map((task) => (
<Todo id={task.id} name={task.name} completed={task.completed} />
));
Ahora la aplicación luce como antes, y nuestro código es menos repetitivo.
Claves únicas
Ahora que React está renderizando nuestras tareas desde un arreglo, tiene que hacer un seguimiento de cuál es cuál para hacerlo correctamente. React intenta hacer esto por si mismo, pero podemos ayudarlo pasándole una prop key
a nuestros componentes <Todo />
. La key
es un prop especial que es administrado por React - no puede usar la palabra key
para cualquier otro propósito.
Debido a que las claves deben ser únicas, vamos a reutilizar el id
de cada objeto de tarea como su clave. Actualize su constante taskList
así.
const taskList = props.tasks.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
/>
));
Siempre debe pasar una clave unica a cualquier cosa que renderice con iteración. Obviamente no cambiará nada en tu navegador, pero si no usas claves únicas, ¡React mostrará una advertencia en la consola y tal vés tu aplicación se comporte extraño!
Creando el resto de componentes de la aplicación
Ahora que hemos resuelto nuestro componente mas importante, podemos dividir el resto de nuestra aplicación en componentes. Recordando que los componentes son piezas obvias de la UI, o piezas reutilizables de la UI, o ambos, podemos hacer 2 componentes mas:
<Form/>
<FilterButton/>
Ya que sabemos que necesitamos a ambos, podemos agrupar la creación de archivos con un comando de la terminal. Ejecuta el comando en tu terminal, teniendo cuidado de estar en la carpeta raiz de tu aplicación.
touch src/components/Form.js src/components/FilterButton.js
El <Form />
Abre components/Form.js
y haz lo siguiente:
- Importa
React
hasta arriba del archivo, como lo hicimos enTodo.js
. - Crea un nuevo componente
Form()
con la misma estructura básica comoTodo()
, y exportalo. - Copia la etiqueta
<form>
y su contenido enApp.js
, y pegalo dentroreturn
del componenteForm()
- Exporta el componente
Form
al final del archivo.
Tu archivo Form.js
debería leerse así:
import React from "react";
function Form(props) {
return (
<form>
<h2 className="label-wrapper">
<label htmlFor="new-todo-input" className="label__lg">
What needs to be done?
</label>
</h2>
<input
type="text"
id="new-todo-input"
className="input input__lg"
name="text"
autoComplete="off"
/>
<button type="submit" className="btn btn__primary btn__lg">
Add
</button>
</form>
);
}
export default Form;
El <FilterButton />
Haz lo mismo que hiciste para crear Form.js
dentro de FilterButton.js
, pero llamando el componente FilterButton()
y copia el HTML del primer botón dentro del <div>
con la class
filters
de App.js
pegalo dentro del return
El archivo debería leerse así:
import React from "react";
function FilterButton(props) {
return (
<button type="button" className="btn toggle-btn" aria-pressed="true">
<span className="visually-hidden">Show </span>
<span>all </span>
<span className="visually-hidden"> tasks</span>
</button>
);
}
export default FilterButton;
Nota:
Podrias notar que estamos cometiendo el mismo error que cometimos por primera vez con el componente <Todo />
, en que cada botón será lo mismo. ¡Esta bien! Vamos arreglarlo mas adelante en Volver a los botones de filtro.
Importando todos nuestros componentes
Hagamos uso de nuestros nuevos componentes.
Agrega más import
en App.js
para importarlos.
Luego, actualice el return
de App()
para que renderice nuestros componentes. Cuando termine, App.js
se leerá así:
import React from "react";
import Form from "./components/Form";
import FilterButton from "./components/FilterButton";
import Todo from "./components/Todo";
function App(props) {
const taskList = props.tasks.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
/>
));
return (
<div className="todoapp stack-large">
<h1>TodoMatic</h1>
<Form />
<div className="filters btn-group stack-exception">
<FilterButton />
<FilterButton />
<FilterButton />
</div>
<h2 id="list-heading">3 tasks remaining</h2>
<ul
role="list"
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading">
{taskList}
</ul>
</div>
);
}
export default App;
Con esto en su lugar, ¡estamos casi listos para abordar algo de interactividad en nuestra aplicación React!