Saltar al contenido

c

Estado del componente, controladores de eventos

Volvamos a trabajar con React.

Comenzamos con un nuevo ejemplo:

const Hello = (props) => {
  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old
      </p>
    </div>
  )
}

const App = () => {
  const name = 'Peter'
  const age = 10

  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <Hello name={name} age={age} />
    </div>
  )
}

Funciones auxiliares del componente

Vamos a expandir nuestro componente Hello para que adivine el año de nacimiento de la persona que recibe la bienvenida:

const Hello = (props) => {
  const bornYear = () => {    const yearNow = new Date().getFullYear()    return yearNow - props.age  }
  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old
      </p>
      <p>So you were probably born in {bornYear()}</p>    </div>
  )
}

La lógica para adivinar el año de nacimiento se divide en su propia función que se llama cuando se renderiza el componente.

La edad de la persona no tiene que pasarse como parámetro a la función, ya que puede acceder directamente a todos los props que se pasan al componente.

Si examinamos nuestro código actual de cerca, notaremos que la función auxiliar está realmente definida dentro de otra función que define el comportamiento de nuestro componente. En la programación Java, definir una función dentro de otra es complejo y engorroso, por lo que no es tan común. En JavaScript, sin embargo, definir funciones dentro de funciones es una técnica de uso común.

Desestructuración

Antes de seguir adelante, veremos una característica pequeña pero útil del lenguaje JavaScript que se agregó en la especificación ES6, que nos permite desestructurar valores de objetos y matrices en la asignación.

En nuestro código anterior, teníamos que hacer referencia a los datos pasados ​​a nuestro componente como props.name y props.age. De estas dos expresiones, tuvimos que repetir props.age dos veces en nuestro código.

Dado que props es un objeto

props = {
  name: 'Arto Hellas',
  age: 35,
}

podemos optimizar nuestro componente asignando los valores de las propiedades directamente en dos variables name y age que luego podemos usar en nuestro código:

const Hello = (props) => {
  const name = props.name  const age = props.age
  const bornYear = () => new Date().getFullYear() - age
  return (
    <div>
      <p>Hello {name}, you are {age} years old</p>      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

Ten en cuenta que también hemos utilizado la sintaxis más compacta para las funciones de flecha al definir la función bornYear. Como se mencionó anteriormente, si una función de flecha consta de una sola expresión, entonces no es necesario escribir el cuerpo de la función entre llaves. En esta forma más compacta, la función simplemente devuelve el resultado de la expresión única.

En resumen, las dos definiciones de función que se muestran a continuación son equivalentes:

const bornYear = () => new Date().getFullYear() - age

const bornYear = () => {
  return new Date().getFullYear() - age
}

La desestructuración facilita aún más la asignación de variables, ya que podemos usarla para extraer y reunir los valores de las propiedades de un objeto en variables separadas:

const Hello = (props) => {
  const { name, age } = props  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>Hello {name}, you are {age} years old</p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

Cuando el objeto que estamos desestructurando tiene los valores

props = {
  name: 'Arto Hellas',
  age: 35,
}

la expresión const { name, age } = props asigna los valores 'Arto Hellas' a name y 35 a age.

Podemos llevar la desestructuración un paso más allá:

const Hello = ({ name, age }) => {  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>
        Hello {name}, you are {age} years old
      </p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

Los props que se pasan al componente ahora se desestructuran directamente en las variables name y age.

Esto significa que en lugar de asignar todo el objeto props a una variable llamada props y luego asignar sus propiedades a las variables name y age

const Hello = (props) => {
  const { name, age } = props

asignamos los valores de las propiedades directamente a las variables al desestructurar el objeto props que se pasa a la función del componente como parámetro:

const Hello = ({ name, age }) => {

Re-renderizado de la página

Hasta ahora, la apariencia de todas nuestras aplicaciones sigue siendo la misma después de la renderización inicial. ¿Qué pasaría si quisiéramos crear un contador donde el valor aumentara en función del tiempo o con el clic de un botón?

Comencemos con lo siguiente. El archivo App.jsx se convierte en:

const App = (props) => {
  const {counter} = props
  return (
    <div>{counter}</div>
  )
}

export default App

Y el archivo main.jsx se convierte en:

import ReactDOM from 'react-dom/client'

import App from './App'

let counter = 1

ReactDOM.createRoot(document.getElementById('root')).render(
  <App counter={counter} />
)

El componente de la aplicación recibe el valor del contador a través del prop counter. Este componente muestra el valor en la pantalla. ¿Qué sucede cuando cambia el valor de counter? Incluso si tuviéramos que agregar lo siguiente

counter += 1

el componente no volverá a renderizar. Podemos hacer que el componente se vuelva a renderizar llamando al método ReactDOM.render por segunda vez, por ejemplo, de la siguiente manera:

let counter = 1

const refresh = () => {
  ReactDOM.createRoot(document.getElementById('root')).render(
    <App counter={counter} />
  )
}

refresh()
counter += 1
refresh()
counter += 1
refresh()

El comando de re-renderizado se ha envuelto dentro de la función refresh para reducir la cantidad de código copiado y pegado.

Ahora el componente se renderiza tres veces, primero con el valor 1, luego 2 y finalmente 3. Sin embargo, los valores 1 y 2 se muestran en la pantalla durante un período de tiempo tan corto que pueden no ser notados.

Podemos implementar una funcionalidad un poco más interesante volviendo a renderizar e incrementando el contador cada segundo usando setInterval:

setInterval(() => {
  refresh()
  counter += 1
}, 1000)

Hacer llamadas repetidas al método ReactDOM.render no es la forma recomendada de volver a renderizar componentes. A continuación, presentaremos una mejor forma de lograr este efecto.

Componente con estado

Todos nuestros componentes hasta ahora han sido simples en el sentido de que no contienen ningún estado que pueda cambiar durante el ciclo de vida del componente.

A continuación, agreguemos estado al componente App de nuestra aplicación con la ayuda del hook de estado de React.

Cambiaremos la aplicación a lo siguiente. main.jsx vuelve a:

import ReactDOM from 'react-dom/client'

import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

y App.jsx cambia a lo siguiente:

import { useState } from 'react'
const App = () => {
  const [ counter, setCounter ] = useState(0)
  setTimeout(    () => setCounter(counter + 1),    1000  )
  return (
    <div>{counter}</div>
  )
}

export default App

En la primera fila, la aplicación importa la función useState:

import { useState } from 'react'

El cuerpo de la función que define el componente comienza con la llamada a la función:

const [ counter, setCounter ] = useState(0)

La llamada a la función agrega state al componente y lo renderiza inicializado con el valor cero. La función devuelve un array que contiene dos elementos. Asignamos los elementos a las variables counter y setCounter usando la sintaxis de asignación por desestructuración mostrada anteriormente.

A la variable counter se le asigna el valor inicial de state, que es cero. La variable setCounter se asigna a una función que se utilizará para modificar el estado.

La aplicación llama a la función setTimeout y le pasa dos parámetros: una función para incrementar el estado del contador y un tiempo de espera de un segundo:

setTimeout(
  () => setCounter(counter + 1),
  1000
)

La función pasada como primer parámetro a la función setTimeout se invoca un segundo después de llamar a la función setTimeout

() => setCounter(counter + 1)

Cuando se llama a la función de modificación de estado setCounter, React vuelve a renderizar el componente, lo que significa que el cuerpo de la función del componente se vuelve a ejecutar:

() => {
  const [ counter, setCounter ] = useState(0)

  setTimeout(
    () => setCounter(counter + 1),
    1000
  )

  return (
    <div>{counter}</div>
  )
}

La segunda vez que la función del componente es ejecutado, llama a la función useState y devuelve el nuevo valor del estado: 1. Al ejecutar el cuerpo de la función nuevamente, también se realiza una nueva llamada de función a setTimeout, que ejecuta el tiempo de espera de un segundo e incrementa el estado counter nuevamente. Debido a que el valor de la variable counter es 1, incrementar el valor en 1 es esencialmente lo mismo que una expresión que establece el valor de counter en 2.

() => setCounter(2)

Mientras tanto, el antiguo valor de counter - "1" - se muestra en la pantalla.

Cada vez que setCounter modifica el estado, hace que el componente se vuelva a renderizar. El valor del estado se incrementará nuevamente después de un segundo y esto continuará repitiéndose mientras la aplicación esté en ejecución.

Si el componente no se renderiza cuando tu crees que debería, o si se renderiza en el "momento incorrecto", puedes depurar la aplicación registrando los valores de las variables del componente en la consola. Si agregamos lo siguiente a nuestro código:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  setTimeout(
    () => setCounter(counter + 1),
    1000
  )

  console.log('rendering...', counter)
  return (
    <div>{counter}</div>
  )
}

Es fácil de seguir y rastrear las llamadas realizadas a la función de renderizado del componente App:

Captura de pantalla de rendering log en herramientas de desarrollo

¿Estaba la consola de tu navegador abierta? Si no lo estaba, entonces promete que esta sera la ultima vez que necesitas que te lo recuerden.

Control de eventos

Ya hemos mencionado a los controladores de eventos algunas veces en la parte 0, que están registrados para ser llamados cuando ocurren eventos específicos. Por ejemplo, la interacción de un usuario con los diferentes elementos de una página web puede provocar que se active una colección de diferentes tipos de eventos.

Cambiemos la aplicación para que aumente el contador cuando un usuario haga clic en un botón, que se implementa con el elemento botón.

Los elementos de botón admiten los llamados eventos de mouse, de los cuales click es el evento más común. El evento de click en un botón también puede ser disparado por el teclado o por una pantalla táctil a pesar del nombre mouse event.

En React, registrar una función de controlador de eventos en el evento click ocurre así:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const handleClick = () => {    console.log('clicked')  }
  return (
    <div>
      <div>{counter}</div>
      <button onClick={handleClick}>        plus      </button>    </div>
  )
}

Establecemos el valor del atributo onClick del botón para que sea una referencia a la función handleClick definida en el código.

Ahora, cada clic del botón plus hace que se llame a la función handleClick, lo que significa que cada evento de clic registrará un mensaje de clicked en la consola del navegador.

La función del controlador de eventos también se puede definir directamente en la asignación de valor del atributo onClick:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => console.log('clicked')}>        plus
      </button>
    </div>
  )
}

Al cambiar el controlador de eventos a la siguiente forma

<button onClick={() => setCounter(counter + 1)}>
  plus
</button>

logramos el comportamiento deseado, lo que significa que el valor de counter aumenta en uno y el componente se vuelve a renderizar.

Agreguemos también un botón para restablecer el contador:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => setCounter(counter + 1)}>
        plus
      </button>
      <button onClick={() => setCounter(0)}>        zero      </button>    </div>
  )
}

Nuestra aplicación ya está lista!

Un controlador de eventos es una función

Definimos los controladores de eventos para nuestros botones donde declaramos sus atributos onClick:

<button onClick={() => setCounter(counter + 1)}>
  plus
</button>

¿Qué pasaría si intentáramos definir los controladores de eventos de una forma más simple?

<button onClick={setCounter(counter + 1)}>
  plus
</button>

Esto rompería completamente nuestra aplicación:

Captura de pantalla de un error de re-renderizado

¿Qué está pasando? Se supone que un controlador de eventos es una función o una referencia de función, y cuando escribimos

<button onClick={setCounter(counter + 1)}>

el controlador de eventos es en realidad una llamada a la función. En muchas situaciones esto está bien, pero no en esta situación particular. Al principio, el valor de la variable counter es 0. Cuando React renderiza el componente por primera vez, ejecuta la llamada de función setCounter(0+1) y cambia el valor del estado del componente en 1. Esto hará que el componente se vuelva a renderizar, react ejecutará la llamada a la función setCounter nuevamente, y el estado cambiará dando lugar a otro re-renderizado..

Definamos los controladores de eventos como lo hicimos antes

<button onClick={() => setCounter(counter + 1)}>
  plus
</button>

Ahora el atributo del botón que define lo que sucede cuando se hace clic en el botón - onClick - tiene el valor () => setCounter (counter + 1). La función setCounter se llama solo cuando un usuario hace clic en el botón.

Por lo general, definir controladores de eventos dentro de las plantillas JSX no es una buena idea. Aquí está bien, porque nuestros controladores de eventos son muy simples.

Vamos a separar a los controladores de eventos en funciones separadas de todas formas:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)  const setToZero = () => setCounter(0)
  return (
    <div>
      <div>{counter}</div>
      <button onClick={increaseByOne}>        plus
      </button>
      <button onClick={setToZero}>        zero
      </button>
    </div>
  )
}

Aquí los controladores de eventos se han definido correctamente. El valor del atributo onClick es una variable que contiene una referencia a una función:

<button onClick={increaseByOne}>
  plus
</button>

Pasando el estado a componentes hijos

Se recomienda escribir componentes de React que sean pequeños y reutilizables en toda la aplicación e incluso en diferentes proyectos. Refactorizemos nuestra aplicación para que esté compuesta por tres componentes más pequeños, un componente para mostrar el contador y dos componentes para los botones.

Primero implementemos un componente Display que sea responsable de mostrar el valor del contador.

Una de las mejores prácticas en React es levantar el estado en la jerarquía de componentes. La documentación dice:

A menudo, varios componentes deben reflejar los mismos datos cambiantes. Recomendamos elevar el estado compartido a su ancestro común más cercano.

Así que coloquemos el estado de la aplicación en el componente App y pasémoslo al componente Display a través de props:

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

Usar el componente es sencillo, ya que solo necesitamos pasarle el estado del counter:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)
  const setToZero = () => setCounter(0)

  return (
    <div>
      <Display counter={counter}/>      <button onClick={increaseByOne}>
        plus
      </button>
      <button onClick={setToZero}>
        zero
      </button>
    </div>
  )
}

Todo sigue funcionando. Cuando se hace clic en los botones y App se vuelve a renderizar, todos sus elementos secundarios, incluido el componente Display, también se vuelven a renderizar.

A continuación, creemos un componente Button para los botones de nuestra aplicación. Tenemos que pasar el controlador de eventos, así como el título del botón a través de las props del componente:

const Button = (props) => {
  return (
    <button onClick={props.onClick}>
      {props.text}
    </button>
  )
}

Nuestro componente App ahora se ve así:

const App = () => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)
  const decreaseByOne = () => setCounter(counter - 1)  const setToZero = () => setCounter(0)

  return (
    <div>
      <Display counter={counter}/>
      <Button        onClick={increaseByOne}        text='plus'      />      <Button        onClick={setToZero}        text='zero'      />           <Button        onClick={decreaseByOne}        text='minus'      />               </div>
  )
}

Dado que ahora tenemos un componente Button fácilmente reutilizable, también hemos implementado una nueva funcionalidad en nuestra aplicación agregando un botón que se puede usar para disminuir el contador.

El controlador de eventos se pasa al componente Button a través de la propiedad handleClick. El nombre del prop en sí no es tan significativo, pero nuestra elección de nombre no fue completamente aleatoria.

El propio tutorial oficial de React sugiere: "En React, es convencional usar nombres onSomething para props que representan eventos y handleSomething para las definiciones de funciones que controlan los eventos."

Los cambios en el estado provocan re-renderizado

Repasemos los principios fundamentales de cómo funciona una aplicación una vez más.

Cuando se inicia la aplicación, se ejecuta el código en App. Este código usa un hook useState para crear el estado de la aplicación, estableciendo un valor inicial de la variable counter. Este componente contiene el componente Display, que muestra el valor del contador, 0, y tres componentes Button. Todos los botones tienen controladores de eventos, que se utilizan para cambiar el estado del contador.

Cuando se hace clic en uno de los botones, se ejecuta el controlador de eventos. El controlador de eventos cambia el estado del componente App con la función setCounter. Llamar a una función que cambia el estado hace que el componente se vuelva a renderizar.

Entonces, si un usuario hace clic en el botón plus, el controlador de eventos del botón cambia el valor de counter a 1, y el componente App se vuelve a renderizar. Esto hace que sus subcomponentes Display y Button también se vuelvan a renderizar. Display recibe el nuevo valor del contador, 1, como prop. Los componentes Button reciben controladores de eventos que pueden usarse para cambiar el estado del contador.

Para asegurarnos de entender como funciona el programa, vamos a agregarle algunos console.log

const App = () => {
  const [counter, setCounter] = useState(0)
  console.log('rendering with counter value', counter)
  const increaseByOne = () => {
    console.log('increasing, value before', counter)    setCounter(counter + 1)
  }

  const decreaseByOne = () => { 
    console.log('decreasing, value before', counter)    setCounter(counter - 1)
  }

  const setToZero = () => {
    console.log('resetting to zero, value before', counter)    setCounter(0)
  }

  return (
    <div>
      <Display counter={counter} />
      <Button onClick={increaseByOne} text="plus" />
      <Button onClick={setToZero} text="zero" />
      <Button onClick={decreaseByOne} text="minus" />
    </div>
  )
}

Ahora veamos que se imprime en la consola cuando se hace clic en los botones plus, zero y minus:

Navegador mostrando la consola con los valores impresos resaltados

No intentes siempre adivinar lo que tu código hace. Justamente lo mejor es usar console.log y ver con tus propios ojos lo que este hace.

Refactorización de los componentes

El componente que muestra el valor del contador es el siguiente:

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

El componente solo usa el campo counter de sus props. Esto significa que podemos simplificar el componente usando desestructuración, así:

const Display = ({ counter }) => {
  return (
    <div>{counter}</div>
  )
}

La función que define el componente contiene solo la declaración return, por lo que podemos definir la función usando la forma más compacta de funciones de flecha:

const Display = ({ counter }) => <div>{counter}</div>

También podemos simplificar el componente Button.

const Button = (props) => {
  return (
    <button onClick={props.onClick}>
      {props.text}
    </button>
  )
}

Podemos usar la desestructuración para obtener solo los campos requeridos de props, y usar la forma más compacta de funciones de flecha:

Nota: Cuando estas construyendo tus propios componentes, puedes nombrar sus controladores de eventos de la manera que quieras, para esto puedes referirte a la documentación de React en Nombrar props de controladores de eventos. Que dice lo siguiente:

Por convención, las props de los controladores de eventos deberían iniciar con on, seguido de una letra mayúscula. Por ejemplo, el prop onClick del componente Button pudo haberse llamado onSmash:

const Button = ({ onClick, text }) => (
  <button onClick={onClick}>
    {text}
  </button>
)

podría también ser llamado de la siguiente manera:

const Button = ({ onSmash, text }) => (
  <button onClick={onSmash}>
    {text}
  </button>
)

Podríamos también simplificar el componente Button un poco más definiendo la declaración return en una sola línea:

const Button = ({ onSmash, text }) => <button onClick={onSmash}>{text}</button>

Nota: Sin embargo, debes ser cuidadoso de no sobresimplificar tus componentes, ya que esto hace que agregar complejidad sea una tarea más tediosa en el futuro.