La idea sobre este post es comenzar a devolver y compartir un poco de lo que he ido aprendido a lo largo de estos años.
Cada vez que busco información la misma está, por lo general, en inglés y creo que esto genera una brecha entre los que pudimos aprender el idioma y los que todavía no han tenido la posibilidad de hacerlo.
En este post, como el título lo indica, voy a estar hablando sobre los fundamentos de React y voy a basarme en el workshop de Kent C. Dodds llamado React Fundamentals, que es parte de una serie llamada Epic React. El contenido se encuentra en su GitHub, donde, si así lo quisieras, podrías clonar el repo y seguir las instrucciones para desarrollarlo.
También agradecer a Bhanu Teja Pachipulusu, ya que ver su post fue lo que me inspiro a hacer el mio propio pero para la comunidad hispanohablante.
¡Comencemos!
Índice:
- ¿Cómo se encarga JavaScript de renderizar un Hello World?
- ¿Cómo React se encarga de renderizar un Hello World?
- Anteriormente mencioné JSX, ¿Qué es?
- Interpolación en JSX
- Componentes custom
- PropTypes
- Fragments
- Estilos
- Formularios
- Refs
- Componentes no controlados
- Componentes controlados
- Renderizando arrays
¿Cómo se encarga JavaScript de renderizar un Hello World?
Supongamos que tenemos el siguiente archivo HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
¿Cómo haríamos con JavaScript para renderizar <div class="container">Hello World</div>
dentro de <div id="root"></div>
?
Deberíamos utilizar la API que JavaScript nos provee para interactuar con el DOM, veamos cómo:
<script type="text/javascript">
// Obtenemos una referencia del elemento root en el DOM.
const rootElement = document.getElementById('root');
// Creamos el nuevo div que va a contener el texto: "Hello World" y la clase "container"
const newElement = document.createElement('div');
newElement.textContent = 'Hello World';
newElement.className = 'container';
// Insertamos el nuevo elemento creado
rootElement.append(newElement);
</script>
Ejemplo en vivo: CodeSandbox
Volver al índice
¿Cómo React se encarga de renderizar un Hello World?
Para comenzar de una manera rápida existe unpkg.com, que nos expone react y react-dom desde una url y nos da acceso a su API para desarrollar. Mejoremos un poco nuestro archivo HTML para darle soporte para React.
Pero te podrás preguntar, ¿Qué son react y react-dom? ¿Qué los diferencia?
- react: te permite crear elementos React, el método que más vamos a utilizar es createElement y, la verdad es que, la mayoría del tiempo lo vamos a estar usando sin saberlo, ya que vamos a estar escribiendo JSX que, por debajo, lo que hace es llamar a este método.
- react-dom: se encarga de renderizar el contenido en el DOM.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@17.0.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.0/umd/react-dom.development.js"></script>
</body>
</html>
Como dijimos previamente, de react vamos a estar utilizando el método React.createElement
y de react-dom vamos a utilizar ReactDOM.render
.
React.createElement
crea y retorna un nuevo elemento React y acepta 3 parámetros:
- el tipo, que puede ser el nombre de la etiqueta HTML como
div
ospan
, un componente React (clase o función) o Fragment, que ya veremos más adelante. - propiedades para el componente, como por ejemplo: className, id, children.
- children, como 'Hello World'. Puede haber 1 o N. Si hay más de uno, se los agrupa como un arreglo.
Dato: dentro de las propiedades podemos incluir una propiedad children, que es lo mismo que pasarlo como tercer parámetro. Debemos tener en cuenta que no tiene sentido utilizar ambas al mismo tiempo, dado que la del tercer parametro tiene mayor peso; si está existiere, sólo se mostraría esa.
Visualicemos:
React.createElement('div', { children: 'props' }, 'Tercer parametro'); // OUTPUT <div>Tercer parametro</div>
Obtendríamos el mismo resultado de cualquiera de las siguientes maneras:
React.createElement('div', { className: 'container', children: 'Hello World' })
// o
React.createElement('div', { className: 'container'}, 'Hello World' })
ReactDOM.render
renderiza un elemento React al DOM en el contenedor suministrado; acepta dos parámetros y uno opcional:
- Elemento a ser renderizado.
- Contenedor donde el elemento va a ser renderizado.
- Callback a ser ejecutado luego de que el componente sea renderizado o actualizado.
Se vería así:
ReactDOM.render(elemento, contenedor[, callback])
Luego de esta introducción a los métodos que vamos a estar utilizando, veamos cómo hacer con React lo mismo que hicimos previamente con JavaScript:
<script type="text/javascript">
// Obtenemos una referencia del elemento root en el DOM.
const rootElement = document.getElementById('root')
// Creamos el nuevo div que va a contener el texto: "Hello World" y la clase "container"
const newElement = React.createElement('div', { className: 'container' }, 'Hello World')
// Insertamos el nuevo elemento creado
ReactDOM.render(newElement, rootElement)
</script>
Ejemplo en vivo: CodeSandbox
¿Qué pasa si no queremos un solo elemento dentro del root?
No hay problema, la prop children soporta que le pasemos un arreglo de elementos:
<script type="text/javascript">
// Obtenemos una referencia del elemento root en el DOM.
const rootElement = document.getElementById('root');
// Creamos nuevos elementos react
const helloElement = React.createElement('div', null, 'Hello');
const worldElement = React.createElement('div', null, 'World');
// Creamos un elemento que va a contener ambos elementos previamente creados
const divElement = React.createElement('div', {
className: 'container',
children: [
helloElement,
" ",
worldElement
]
});
// Insertamos el nuevo elemento creado
ReactDOM.render(divElement, rootElement);
</script>
Recuerden, podemos hacerlo de cualquiera de estas dos formas:
React.createElement('div', {
className: 'container',
children: [
helloElement,
worldElement
]
});
// o
React.createElement('div', {
className: 'container',
}, helloElement, worldElement);
Ejemplo en vivo: CodeSandbox
Volver al índice
Anteriormente mencioné JSX, ¿Qué es?
JSX es una extensión de JavaScript creada por Facebook. A primera vista nos puede parecer que estamos mezclando JavaScript con HTML, lo que nos hace más fácil desarrollar los componentes de React pero, en realidad, es sólo azúcar sintáctico para la función de React.createElement(component, props, ...children)
.
Tal como hicimos anteriormente con React, vamos a tener que agregar Babel a nuestra web para poder utilizar JSX, ya que no es código JavaScript válido por sí mismo.
Mejoremos otra vez nuestro HTML para poder practicar algunos conceptos:
Podemos utilizar Babel - Try out para ir viendo cómo Babel transpila nuestro código y su resultado final.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@17.0.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.12.4/babel.js"></script>
<script type="text/babel">
// Con type="text/babel" le estamos diciendo al browser que este script debe ser transpilado por Babel
// ...nuestro código
</script>
</body>
</html>
Hay que tener en cuenta ciertas cosas a la hora de escribir JSX:
- Tenemos ciertas palabras reservadas en JavaScript que son iguales a algunos atributos HTML, por eso, al utilizarlos, tenemos que cambiarlos por su semejante en JSX. Por ejemplo, class por className o for por htmlFor.
- Los atributos en JSX, a excepción de los aria-*, deben ser escritos en camelCase.
- Debemos cerrar todas las etiquetas. Por ejemplo,
<span></span>
o<img />
. - Nunca olvidar que JSX espera que retornemos un solo elemento.
Sigamos avanzando con los ejemplos anteriores para ponerlo en práctica. ¿Recuerdan el <div class=”container”>Hello World</div>
?
Vamos a escribirlo tanto con React.createElement como con JSX:
// React.createElement
const newElement = React.createElement('div', { className: 'container' }, 'Hello World');
// JSX
const newElement = <div className="container">Hello World</div>;
Veamos ahora cómo haríamos el ejemplo con dos children:
// React.createElement
const helloElement = React.createElement('div', null, 'Hello');
const worldElement = React.createElement('div', null, 'World');
const divElement = React.createElement('div', {
className: 'container',
}, helloElement, worldElement);
// JSX
const divElement = (
<div className="container">
<div>Hello</div>
<div>World</div>
</div>
);
¿No es mucho más fácil de entender e imaginar el resultado final?
Ejemplo en vivo: CodeSandbox
Volver al índice
Interpolación en JSX
Al JSX estár escrito en JavaScript, podemos hacer algunas cosas muy interesantes. Por ejemplo, utilizar JavaScript dentro del JSX, pero para poder hacer esto utilizaremos {}
para envolver nuestro JavaScript y esto le notifica a Babel que acá se está interpolando JSX y JavaScript.
Veámoslo en ejemplo. Tomemos el siguiente código:
const newElement = <div className="container">Hello World</div;
Digamos que queremos que, tanto la clase como su children, sean dinámicos, que queramos definirlos como variables por fuera. Podríamos hacer algo así:
const myClass = 'container';
const children = 'Hello world';
const newElement = <div className={myClass}>{children}</div>;
Y eso es todo, ¿no es simple y poderoso?
Puntos a tener en cuenta:
- No podemos hacer declaraciones dentro de los
{}
:
const newElement = (
<div className={myClass}>
{if (children) {
...code
}
}
</div>
);
En cambio sí podemos utilizar expresiones:
const newElement = (
<div className={myClass}>
// Ternarios
{children
? `El hijo es: ${children}`
: 'No tiene hijo'
}
</div>
);
Ejemplo en vivo: CodeSandbox
¿Qué pasa si tenemos un objeto de props que queremos aplicar a nuestro JSX?
const newElementProps = {
className: 'myClass',
children: 'Hello World',
};
Aplicando lo visto anteriormente, podríamos hacer:
const newElement = (
<div className={newElementProps.className}>
{newElementProps.children}
</div>;
);
¿No es tedioso tener que estar utilizando siempre el objeto newElementProps
, y cada una de sus keys para definir cada una de las props del elemento? Imaginemos si tenemos 30 props.
¿No te gustaría sólo decir: "okey, quiero que todas las props del objeto newElementProps
sean aplicadas acá"? Te tengo una buena noticia: existe la forma, ¡¡gracias spread!!
// React.createElement
const newElement = React.createElement('div', newElementProps);
// ¿Qué pasa si por ejemplo, el elemento ya tiene una prop id?
const newElement = React.createElement('div', {id: 'my-id', ...newElementProps });
// JSX
const newElement = <div {...newElementProps} />;
// JSX y con prop id
const newElement = <div id="my-id" {...newElementProps} />;
¿Qué deberíamos tener en cuenta a la hora de usar el spread?
Hay que tener en cuenta en qué posición realizamos el spread de las props. Éste sobreescribe las de igual nombre, si es realizado luego de la definición. Por ejemplo:
const props = {
className: 'myClass',
id: 'my-id-for-spread'
};
const newElement = <div {...props } id="my-id">Hello</div>;
// El resultado en este caso sería:
// <div className="myClass" id="my-id">Hello</div>
// En cambio, si lo aplicamos de la siguiente manera:
const newElement2 = <div id="my-id" {...props }>Hello</div>;
// El resultado en este caso sería:
// <div className="myClass" id="my-id-for-spread">Hello</div>
Ejemplo en vivo: CodeSandbox
Volver al índice
Componentes custom
Por lo general, cuando comenzamos a desarrollar una aplicación, nos damos cuenta que existen elementos comunes que vamos a estar repetiendo, sean botones, mensajes, etc.
JSX nos permite utilizar JavaScript para crear nuestros componentes. Por ejemplo, abstraer las cosas comunes en una función que retorna JSX, o sea, un React Element.
Vayamos paso a paso. Supongamos que tenemos el siguiente caso:
<div className="container">
<div className="message">Hello</span>
<div className="message">World</span>
</div>
Claramente observamos que los elementos div
son exactamente iguales, lo único que cambia es el texto que está mostrando. Entonces, ¿qué haríamos?
Abstraer ese elemento a una función:
function message(text) {
return <div className="message">{text}</div>
}
Ahora podríamos aplicar lo que aprendimos anteriormente en JSX y la interpolación:
<div className="container">
{message('Hello'})}
{message('World'})}
</div>
Esto funciona perfectamente, pero podríamos mejorar un poco el código pensando en cómo funciona React.createElement
, que utiliza la prop children para renderizar.
// Destructurando las props nos da una idea rápida
// de las props que el componente está esperando
function message({ children }) {
return <div className="message">{children}</div>
}
<div className="container">
{message({ children: 'Hello' })}
{message({ children: 'World' })}
</div>
Anteriormente dijimos que prácticamente no vamos a estar escribiendo React.createElement
por todos lados, sino que vamos a estar utilizando JSX.
En los ejemplos anteriores vimos que:
// React.createElement:
const newElement = React.createElement('div', { className: 'container' }, 'Hello');
// JSX:
const newElement = <div className="container">Hello</div>;
Entonces, podríamos suponer que, siguiendo ese ejemplo, lo que debemos hacer es lo siguiente:
<div className="container">
<message>Hello</message>
{React.createElement(message, { children: 'World' })}
</div>
A primera vista parecería que está funcionando pero, cuando vamos a las tools y examinamos los elementos, vemos que, en vez de estar renderizando <div className="message">Hello</div>
, lo que está pasando es que se está renderizando <message>Hello</message>
, que, claramente, no es lo que estamos esperando que pase.
Si vamos a la consola, nos vamos a encontrar:
La razón de esto es cómo Babel compila nuestro código:
Podemos observar que, cuando utilizamos <message>
, Babel, en vez de compilar a React.createElement(message)
, lo que hace es compilar a React.createElement('message')
.
Entonces, para solventar esto, lo que debemos hacer es exactamente lo que dice el warning de la consola: utilizar mayúsculas para nuestros componentes.
function Message({ children }) {
return <div className="message">{children}</div>
}
<div className="container">
<Message>Hello</Message>
<Message>World</Message>
</div>
Ahora, si volvemos a ir a las tools y examinar los elementos, vemos que todo se está renderizando correctamente.
Al poner nuestro componente en mayúscula, Babel identifica que es un componente de React y, en lugar de al type agregarle comillas, lo pasa directamente para que el browser pueda manejar esa referencia en el scope.
Ejemplo en vivo: CodeSandbox
Volver al índice
PropTypes, ¿qué son? y ¿para qué sirven?
Hay aplicaciones en donde se utiliza Flow o TypeScript para verificar los tipos en JavaScript, pero no es una regla y React nos provee de PropTypes para verificarlos si así lo quisiéramos.
Comencemos modificando el componente custom que creamos anteriormente para que acepte un nombre y un saludo:
function Message({ nombre, saludo }) {
return (
<div className="message">
Hey {nombre}, {saludo}
</div>
);
}
<div className="container">
<Message nombre="Denis" saludo="Como andas?" />
<Message saludo="Como andas?" />
</div>
// OUTPUT
Hey Denis, Como andas?
Hey , Como andas?
Como podemos observar, no se ve bien. No sería un comportamiento esperado para un componente pero, al mismo tiempo, no nos está diciendo que está fallando o cómo deberíamos solventar este error.
Sería interesante poder hacer algo similar a lo que pasaba cuando tratábamos de renderizar un elemento React en minúsculas y nos decía que debíamos utilizar mayúsculas, ¿no?
Para eso, existen las PropTypes. Comencemos forzando al componente a mostrarnos un error si la prop nombre no es un string:
function Message({ nombre, saludo }) {
return (
<div className="message">
Hey {nombre}, {saludo}
</div>
);
}
Message.propTypes = {
nombre(props, propName, componentName) {
if (typeof props[propName] !== 'string') {
return new Error(`El componente ${componentName} necesita que la prop ${propName} sea de tipo "string" pero recibió ${typeof props[propName]}`);
}
}
};
Este objeto propTypes es algo que React va a referenciar cuando renderice el componente y va a pasar las props por las funciones que nosotros le proveamos al objeto para realizar las validaciones correspondientes.
Nótese que la función, en este caso, matchea con la prop en cuestión. Como primer parámetro va a recibir todas las props, como segúndo parámetro, el nombre de la prop en cuestión, y como tercer parámetro, el nombre del componente.
Ahora, si volvemos a inspeccionar la consola, vamos a notar que hay un warning:
¡Perfecto! ¡Es lo que buscabamos!
Ahora hagamos lo mismo con la prop saludo. Si lo pensamos un minuto, deberíamos utilizar la misma funcion. Entonces, para no repetir código, lo que vamos a hacer es crear un objeto PropTypes que contenga nuestras validaciones:
const PropTypes = {
string(props, propName, componentName) {
if (typeof props[propName] !== 'string') {
return new Error(`El componente ${componentName} necesita que la prop ${propName} sea de tipo "string" pero recibió ${typeof props[propName]}`);
}
}
};
Message.propTypes = {
nombre: PropTypes.string,
saludo: PropTypes.string
};
Ya tenemos nuestro componente que va a validar que las dos props que necesita para funcionar correctamente sean del tipo que nosotros definimos, y sino es así, que avise por qué está fallando y cómo solucionarlo.
Como estos casos son muy comunes, el equipo de React desarrolló y mantiene una librería llamada prop-types que, básicamente, funciona tal cuál nosotros lo implementamos.
Si quisíeramos implementarla, podríamos copiar el script que nos proveen y agregarlo en nuestra página junto con los demás scripts.
<script src="https://unpkg.com/prop-types@15.7.2/prop-types.js"></script>
Luego, deberíamos remover nuestro objecto PropTypes y utilizar la variable global que el script de prop-types agregó a la página. Hay que tener encuenta que, por defecto, la librería te muestra un error si la propType no tiene un valor por defecto y no es requerida.
function Message({ nombre, saludo }) {
return (
<div className="message">
Hey {nombre}, {saludo}
</div>
);
}
// En nuestro caso ambos valores son requeridos para funcionar
Message.propTypes = {
nombre: PropTypes.string.isRequired,
saludo: PropTypes.string.isRequired
};
PropTypes sólo funciona en ambientes de desarrollo por cuestiones de performance.
Ejemplo en vivo: CodeSandbox
Volver al índice
Fragments
Prestemos atención al siguiente caso:
Nos gustaría obtener el siguiente resultado final:
Comencemos obteniendo la referencia del nodo root y creando los elementos que queremos insertarle:
const rootElement = document.getElementById('root');
const helloElement = <div className="hello">Hello</div>;
const worldElement = <div className="world">World</div>;
Hasta acá perfecto pero, ¿ahora? ¿Cómo agregamos estos dos elementos?
Sabemos que, para renderizar un elemento en otro, utilizamos ReactDom.render(element, contenedor)
. Pero, también sabemos que este método espera que le pasemos un solo elemento como argumento para renderizar en el contenedor.
Una forma de resolverlo podría ser:
const newElement = (
<div>
<div className="hello">Hello</div>
<div className="world">World</div>
</div>
)
Pero, el resultado final, no sería el que nosotros esperamos. Estaríamos viendo:
Esto no es lo que queríamos realmente, entonces.. ¿cómo podemos resolverlo? Si, con Fragments.
React Fragments fue introducido en la version 16.2.0 y llegó a resolver exactamente este problema. Ahora somos capaces de renderizar múltiples elementos envolviéndolos en <React.Fragment></React.Fragment>
:
const newElement = (
<React.Fragment>
<div className="hello">Hello</div>
<div className="world">World</div>
</React.Fragment>
);
React va a ignorar <React.Fragment>
cuando renderice el componente, por lo cuál, el resultado final sí quedaría como lo estabamos esperando.
Es realmente útil cuando la estructura del DOM es realmente importante. Por ejemplo, si estás utilizando Flexbox o Grid en un elemento padre y el componente hijo tiene N elementos y estos tienen que alinearse de determinada manera.
También podemos escribirlos de la siguiente manera:
const newElement = (
<>
<div className="hello">Hello</div>
<div className="world">World</div>
</>
);
Y se comporta de la misma manera que vimos anteriormente. Babel compila ambos casos a React.createElment(React.Fragment, null, ...childrens)
Ejemplo en vivo: CodeSandbox
Volver al índice
Estilos
Podemos estilar nuestros componentes principalmente de dos maneras:
- Inline css
- Regular css
Comencemos por el primero. "inline css" es la manera de agregar estilos en línea con la propiedad style.
En HTML lo haríamos de la siguiente manera:
<div style="color: blue; font-size: 16px">Blue text</div>
En cambio, en React, la prop style no está esperando un string, sino que está esperando un objeto:
const myStyle = {
color: 'blue',
fontSize: '16px'
}
<div style={myStyle}>Blue text</div>
// O podríamos escribirlo en linea
<div style={{ color: 'blue', fontSize: '16px' }}>Blue text</div>
Nótese que cambia la manera en la que escribimos las propiedades de css en React. Las debemos escribir en camelCase en lugar de kebab-cased. Podemos notar esta diferencia en el primer ejemplo dónde, en HTML, escribimos font-size y, en React, fontSize.
Es importante entender que los valores de las propiedades del objeto style sólo pueden ser string o number, dado que espera un objeto JavaScript válido. Entonces, no es posible escribir cosas como fontSize: 16px
, lo correcto es fontSize: '16px'
.
Cuando hablamos de "regular css", hablamos de la prop className que espera un string con el nombre de la o las clases que queremos aplicarle.
<div className=”blue-text”>Blue text</div>
Es muy común que las clases de css dependan también de las props o del estado del componente. Por ejemplo:
function Text({ bold }) {
const className = bold ? 'blue-text--bold' : 'blue-text';
return <span className={className}>Blue text</span>
}
Dependiendo del valor de la prop bold, podríamos obtener dos resultados:
- bold es true:
<span className="blue-text--bold">Blue text</span>
- bold es false:
<span className="blue-text">Blue text</span>
Tenemos que tener en cuenta a la hora de combinar ambas maneras de estilar nuestros componentes, la especificidad, dado que style tiene una especificidad mayor a className. ¿Qué quiero decir con esto?
Supongamos que tenemos la clase:
.blue-text {
color: blue;
font-size: 16px;
}
Y vamos a aplicarla a:
<span className="blue-text" style={{ fontSize: '20px' }}>
Blue text
</span>
El resultado final tendrá color: blue
pero, en vez de contener font-size: 16px
, tendrá font-size: 20px
.
Ejemplo en vivo: CodeSandbox
Volver al índice
Formularios
Partamos del siguiente formulario:
<form>
<div>
<label htmlFor="usernameId">Nombre de usuario:</label>
<input id="usernameId" type="text" name="username" />
</div>
<button type="submit">Submit</button>
</form>
Lo que ahora necesitamos es una función para manejar el submit de este formulario:
function handleSubmit() {
console.log('Enviado');
}
Una vez que tenemos la función, lo que hacemos es decirle a React que, cuando cree este formulario, queremos que ejecute esta función cuando el evento onSubmit es ejecutado.
Actualicemos nuestro form:
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameId">Nombre de usuario:</label>
<input id="usernameId" type="text" name="username" />
</div>
<button type="submit">Submit</button>
</form>
Cuando enviamos el formulario, el navegador, por defecto, va a hacer una petición GET con los valores del formulario como parámetros de consulta (query params) en la URL y, lo que vamos a ver, es que la página se refresca completamente.
Para evitar este comportamiento por defecto, modificamos nuestro handleSubmit
para preveenirlo:
function handleSubmit(event) {
event.preventDefault();
console.log('Enviado');
}
Claramente, no sólo vamos a querer prevenir el evento y hacer un console.log()
. Probablemente querramos acceder a los valores de los inputs, por ejemplo, y ésto podemos lograrlo de distintas maneras:
-
event.target
nos facilita un array con los elementos ordenados como aparecen en el form. Por ejemplo, en este caso, sólo tenemos un elemento, por lo que podríamos acceder al valor haciendoevent.target[0].value
. - ¿Qué pasa si tenemos más de un elemento? Se va a tornar inmantenible hacerlo de la manera anterior. Dentro de
event.target
, vamos a encontrar que tiene una propiedad elements donde podemos encontrar los elementos tanto por sus propiedades id, como por name. Esto nos facilita hacerevent.target.elements.usernameId.value
oevent.target.elements.username.value
, para obtener el valor actual de nuestro input.
Ejemplo en vivo: CodeSandbox
Volver al índice
Refs
const myRef = useRef(initialValue);
Existe otra manera de obtener la referencia a un elemento de React y es utilizando Refs.
Ésta devuelve un objeto mutable, cuya propiedad .current
se inicializará con el argumento sumistrado (initialValue).
Hay que destacar que se mantiene consistente entre los rerenders del componente y, además, modificarlo no hará que el componente se rerenderice.
Cuando asignamos nuestra ref a un elemento React, .current
va a tener una referencia a ese elemento.
Entonces, si relacionamos esto con el ejemplo anterior, deducimos que podemos obtener el valor del input utilizando refs. Veamos cómo:
function Form() {
const usernameRef = React.useRef(null);
function handleSubmit(event) {
event.preventDefault();
console.log(usernameRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameId">Nombre de usuario:</label>
<input
id="usernameId"
name="username"
ref={usernameRef}
type="text”
/>
</div>
<button type="submit">Submit</button>
</form>
)
}
Ejemplo en vivo: CodeSandbox
Volver al índice
Componentes no controlados
En nuestro ejemplo del formulario estamos dejando que el propio DOM maneje los datos de los formularios. En HTML, los elementos como <input>
, <textarea>
y <select>
mantienen sus propios estados, actualizándolos de acuerdo a la interacción del usuario y podemos acceder a ellos como vimos previamente con Refs.
Volver al índice
Componentes controlados
Los componentes controlados manejan su propio estado y sólo se actualizan con setState
en los componentes de clase, y con el hook de useState
en los componentes funcionales.
Veamos cómo transformamos nuestro componente Form en un componente controlado:
function Form() {
const [username, setUsername] = useState('');
function handleSubmit(event) {
event.preventDefault();
console.log(username);
}
function handleChange(event) {
setUsername(event.target.value);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameId">Nombre de usuario:</label>
<input
id="usernameId"
name="username"
onChange={handleChange}
type="text”
value={username}
/>
</div>
<button type="submit">Submit</button>
</form>
)
}
Ahora nosotros tenemos control total sobre el input
, y el valor mostrado, será el que está siendo almacenado en username
pasándoselo a la prop value. Para esto estamos utilizando el evento onChange
, que nos permite capturar el evento cada vez que una tecla es oprimida dentro del input
y actualizar el valor del input
utilizando el método setUsername
que definimos previamente.
Tener el valor del input
en el estado del componente, nos da la flexibilidad de que si ahora queremos compartir este valor con otros elementos de la UI, podemos hacerlo. Por ejemplo, un popup o también podemos reiniciarlo desde otros manejadores de eventos.
Juguemos un poco con el handleChange
para que sea un poco más útil y, por ejemplo, setee un error cuando el usuario intenta ingresar un carácter no válido:
function Form() {
const [username, setUsername] = useState('');
const [error, setError] = useState('');
function handleSubmit(event) {
event.preventDefault();
console.log(username);
}
function handleChange(event) {
const { target: { value } } = event;
// solo queremos que el usuario ingrese letras, de lo contrario mostramos un mensaje de error
const esValido = /^[a-zA-Z]+$/g.test(value);
setError(esValido ? null : 'El nombre de usuario solo permite letras')
setUsername(value);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameId">Nombre de usuario:</label>
<input
id="usernameId"
name="username"
onChange={handleChange}
type="text"
value={username}
/>
{error ? <div role="alert">{error}</div> : null}
</div>
<button type="submit">Submit</button>
</form>
)
}
Ejemplo en vivo: CodeSandbox
Volver al índice
Renderizando arrays
Cuando queremos renderizar una lista de elementos, React requiere que le pasemos como prop una key
única para cada elemento. Esto le permite identificarlo y saber qué tiene que hacer con él en cada render.
function ListaDeUsuarios({ usuarios }) {
const listaDeUsuarios = usuarios.map(usuario => <li>{usuario}</li>);
return <ol>listaDeUsuarios</ol>;
}
const usuarios = ['Juan', 'Pedro', 'Sofia', 'Valentina'];
ReactDom.render(
<ListaDeUsuarios usuarios={usuarios} />,
document.getElementById('root')
);
Cuando este código sea ejecutado, vamos a recibir una alerta de que cada ítem de la lista debe tener una key
asignada. Usualmente vamos a utilizar los IDs de nuestros datos. Como última instancia, podemos utilizar el índice, pero vale aclarar que, por defecto, es lo que hace React cuando no encuentra una y ésto no es una buena práctica.
Las keys
ayudan a React a identificar qué ítems han cambiado, son agregados o son eliminados. Deben ser únicas solo entre hermanos, pero no necesariamente deben ser únicas globalmente.
const listaUsuarios = [
{ id: "juan1", nombre: "Juan" },
{ id: "pedro2", nombre: "Pedro" },
{ id: "sofia3", nombre: "Sofia" },
{ id: "valentina4", nombre: "Valentina" }
];
function ListaUsuarios() {
const [usuarios, setUsuarios] = React.useState(listaUsuarios);
function eliminarUsuario(usuario) {
setUsuarios(
usuarios.filter(
(usuariosActuales) => usuariosActuales.id !== usuario.id
)
);
}
function resetLista() {
setUsuarios(listaUsuarios);
}
return (
<div>
<ul style={{ listStyle: "none" }}>
{usuarios.map((usuario) => (
<li>
<button onClick={() => eliminarUsuario(usuario)}>Eliminar</button>
<label htmlFor={usuario.id}>{usuario.nombre}</label>
<input id={usuario.id} defaultValue={usuario.nombre} />
</li>
))}
</ul>
<button onClick={() => resetLista()}>Reset</button>
</div>
);
}
Como primera instancia podemos observar que la consola nos está arrojando un error:
Esto ocurre por lo que dijimos antes: React espera que le pasamos una key única para cada elemento de una lista.
Además, si jugamos un poco con el código anterior removiendo usuarios en distinto orden, podemos observar cómo los valores por defecto de un elemento dejan de coincidir. Esto ocurre porque, al no estar proporcionándole una key única al elemento, React no sabe a ciencia cierta qué elemento fue modificado y esto puede derivar en comportamientos inesperados en el estado de un elemento no controlado.
Con tan sólo agregarle una key única a cada elemento de la lista, solventamos el error de la consola y, a la vez, podemos observar como todo funciona de manera correcta:
{usuarios.map((usuario) => (
<li key={usuario.id}>
<button onClick={() => eliminarUsuario(usuario)}>Eliminar</button>
<label htmlFor={usuario.id}>{usuario.nombre}</label>
<input id={usuario.id} value={usuario.nombre} />
</li>
))}
Ejemplo en vivo: CodeSandbox
Volver al índice
Gracias por su tiempo. Espero que les sea de utilidad y puedan aplicarlo en el futuro.
Saludos!
Links y referencias:
- Epic React
- React Fundamentals - Repo
- React Fundamentals - Bhanu Teja
- MDN Web Doc
- Documentación de React
- React sin JSX
- React con JSX
- JSX en profundidad
- Componentes custom
- PropTypes
- Fragments
- Inline css
- Regular css
- Formularios
- Refs
- Componentes no controlados
- Componentes controlados
- Renderizando arrays
Top comments (0)