DEV Community

Cover image for Fundamentos de React
cdenis
cdenis

Posted on

Fundamentos de React

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?

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>
Enter fullscreen mode Exit fullscreen mode

¿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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 o span, 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' })
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

¿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>;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

En cambio sí podemos utilizar expresiones:

const newElement = (
  <div className={myClass}>
    // Ternarios
    {children
      ? `El hijo es: ${children}`
      : 'No tiene hijo'
    }
   </div>
);
Enter fullscreen mode Exit fullscreen mode

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',
};
Enter fullscreen mode Exit fullscreen mode

Aplicando lo visto anteriormente, podríamos hacer:

const newElement = (
  <div className={newElementProps.className}> 
    {newElementProps.children}
  </div>;
);
Enter fullscreen mode Exit fullscreen mode

¿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} />;
Enter fullscreen mode Exit fullscreen mode

¿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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

Ahora podríamos aplicar lo que aprendimos anteriormente en JSX y la interpolación:

<div className="container">
  {message('Hello'})}
  {message('World'})}
</div>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

elements

Si vamos a la consola, nos vamos a encontrar:
warning

La razón de esto es cómo Babel compila nuestro código:
babel

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>
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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]}`);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

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:
errorMessage

¡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
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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:

fragments

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>;
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

Pero, el resultado final, no sería el que nosotros esperamos. Estaríamos viendo:

output-fragments

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
  </>
);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

Dependiendo del valor de la prop bold, podríamos obtener dos resultados:

  1. bold es true: <span className="blue-text--bold">Blue text</span>
  2. 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;
}
Enter fullscreen mode Exit fullscreen mode

Y vamos a aplicarla a:

<span className="blue-text" style={{ fontSize: '20px' }}>
  Blue text
</span>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Lo que ahora necesitamos es una función para manejar el submit de este formulario:

function handleSubmit() {
  console.log('Enviado');
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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 haciendo event.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 hacer event.target.elements.usernameId.value o event.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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Como primera instancia podemos observar que la consola nos está arrojando un error:
key-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>
))}
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (0)