Saltar al contenido

d

Un estado más complejo, depurando aplicaciones React

Estado complejo

En nuestro ejemplo anterior el estado de la aplicación era simple ya que estaba compuesto por un solo entero. ¿Y si nuestra aplicación requiere un estado más complejo?

En la mayoría de los casos, la mejor, y más fácil, manera de lograr esto es usando la función useState varias veces para crear "partes" de estado separadas.

En el siguiente código creamos dos partes de estado para la aplicación llamada left y right que obtienen el valor inicial de 0:

const App = () => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)

  return (
    <div>
      {left}
      <button onClick={() => setLeft(left + 1)}>
        left
      </button>
      <button onClick={() => setRight(right + 1)}>
        right
      </button>
      {right}
    </div>
  )
}

El componente obtiene acceso a las funciones setLeft y setRight que puede usar para actualizar las dos partes del estado.

El estado del componente o una parte de su estado puede ser de cualquier tipo. Podríamos implementar la misma funcionalidad guardando el recuento de clics de los botones left y right en un solo objeto:

{
  left: 0,
  right: 0
}

En este caso, la aplicación se vería así:

const App = () => {
  const [clicks, setClicks] = useState({
    left: 0, right: 0
  })

  const handleLeftClick = () => {
    const newClicks = {
      left: clicks.left + 1,
      right: clicks.right
    }
    setClicks(newClicks)
  }

  const handleRightClick = () => {
    const newClicks = {
      left: clicks.left,
      right: clicks.right + 1
    }
    setClicks(newClicks)
  }

  return (
    <div>
      {clicks.left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {clicks.right}
    </div>
  )
}

Ahora el componente solo tiene una parte de estado y los controladores de eventos deben encargarse de cambiar el estado completo de la aplicación.

El controlador de eventos se ve un poco desordenado. Cuando se hace clic en el botón izquierdo, se llama a la siguiente función:

const handleLeftClick = () => {
  const newClicks = {
    left: clicks.left + 1,
    right: clicks.right
  }
  setClicks(newClicks)
}

El siguiente objeto se establece como el nuevo estado de la aplicación:

{
  left: clicks.left + 1,
  right: clicks.right
}

El nuevo valor de la propiedad left ahora es el mismo que el valor de left + 1 del estado anterior, y el valor de la propiedad right es el mismo que el valor de la propiedad right del estado anterior.

Podemos definir el nuevo objeto de estado de una manera más clara utilizando la sintaxis de object spread que se agregó a la especificación del lenguaje en el verano de 2018:

const handleLeftClick = () => {
  const newClicks = {
    ...clicks,
    left: clicks.left + 1
  }
  setClicks(newClicks)
}

const handleRightClick = () => {
  const newClicks = {
    ...clicks,
    right: clicks.right + 1
  }
  setClicks(newClicks)
}

La sintaxis puede parecer un poco extraña al principio. En la práctica, { ...clicks } crea un nuevo objeto que tiene copias de todas las propiedades del objeto clicks. Cuando especificamos una propiedad en particular, por ejemplo, right en { ...clicks, right: 1 }, el valor de la propiedad right en el nuevo objeto será 1.

En el ejemplo anterior, esto:

{ ...clicks, right: clicks.right + 1 }

crea una copia del objeto clicks donde el valor de la propiedad right aumenta en uno.

No es necesario asignar el objeto a una variable en los controladores de eventos y podemos simplificar las funciones a la siguiente forma:

const handleLeftClick = () =>
  setClicks({ ...clicks, left: clicks.left + 1 })

const handleRightClick = () =>
  setClicks({ ...clicks, right: clicks.right + 1 })

Algunos lectores podrían preguntarse por qué no actualizamos el estado directamente, así:

const handleLeftClick = () => {
  clicks.left++
  setClicks(clicks)
}

La aplicación parece funcionar. Sin embargo, está prohibido en React mutar el estado directamente, ya que puede provocar efectos secundarios inesperados. El cambio de estado siempre debe realizarse estableciendo el estado en un nuevo objeto. Si las propiedades del objeto de estado anterior no se modifican, simplemente deben copiarse, lo que se hace copiando esas propiedades en un nuevo objeto y estableciendo eso como el nuevo estado.

Almacenar todo el estado en un solo objeto de estado es una mala elección para esta aplicación en particular; no hay ningún beneficio aparente y la aplicación resultante es mucho más compleja. En este caso, almacenar los contadores de clics en estados separados es una opción mucho más adecuada.

Hay situaciones en las que puede resultar beneficioso almacenar una parte del estado de la aplicación en una estructura de datos más compleja. La documentación oficial de React contiene una guía útil sobre el tema.

Manejo de arrays

Agreguemos un fragmento de estado a nuestra aplicación que contenga un array allClicks que recuerda cada clic que ha ocurrido en la aplicación.

const App = () => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])
  const handleLeftClick = () => {    setAll(allClicks.concat('L'))    setLeft(left + 1)  }
  const handleRightClick = () => {    setAll(allClicks.concat('R'))    setRight(right + 1)  }
  return (
    <div>
      {left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {right}
      <p>{allClicks.join(' ')}</p>    </div>
  )
}

Cada clic se almacena en una pieza de estado separada llamada allClicks que se inicializa como un array vacío:

const [allClicks, setAll] = useState([])

Cuando se hace clic en el botón left, agregamos la letra L al array allClicks:

const handleLeftClick = () => {
  setAll(allClicks.concat('L'))
  setLeft(left + 1)
}

La parte del estado almacenada en allClicks ahora está configurada para ser un array que contiene todos los elementos del array de estado anterior más la letra L. La adición del nuevo elemento al array se logra con el método concat, que no muta el array existente, sino que devuelve una nueva copia del array con el elemento agregado.

Como se mencionó anteriormente, también es posible en JavaScript agregar elementos a un array con el método push. Si agregamos el elemento empujándolo al array allClicks y luego actualizando el estado, la aplicación aún parecería funcionar:

const handleLeftClick = () => {
  allClicks.push('L')
  setAll(allClicks)
  setLeft(left + 1)
}

Sin embargo, no hagas esto. Como se mencionó anteriormente, el estado de los componentes de React, como allClicks, no debe modificarse directamente. Incluso si el estado mutado parece funcionar en algunos casos, puede provocar problemas que son muy difíciles de depurar.

Echemos un vistazo más de cerca a cómo se muestra el historial de clics en la página:

const App = () => {
  // ...

  return (
    <div>
      {left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {right}
      <p>{allClicks.join(' ')}</p>    </div>
  )
}

Llamamos al método join en el array allClicks, que une todos los elementos en un solo string, separados por el string pasado como parámetro de la función, que en nuestro caso es un espacio vacío.

La actualización del estado es asíncrona

Ampliemos la aplicación para que realice un seguimiento del número total de veces que los botones son presionados, cuyo valor siempre se actualiza cuando se presionan los botones:

const App = () => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])
  const [total, setTotal] = useState(0)
  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    setLeft(left + 1)
    setTotal(left + right)  }

  const handleRightClick = () => {
    setAll(allClicks.concat('R'))
    setRight(right + 1)
    setTotal(left + right)  }

  return (
    <div>
      {left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {right}
      <p>{allClicks.join(' ')}</p>
      <p>total {total}</p>    </div>
  )
}

La solución no funciona del todo:

Navegador mostrando 2 left|right 1, RLL total 2

El número total de pulsaciones de botones es constantemente uno menos que la cantidad real de pulsaciones, por alguna razón.

Agreguemos un par de console.log al controlador de eventos:

const App = () => {
  // ...
  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    console.log('left before', left)    setLeft(left + 1)
    console.log('left after', left)    setTotal(left + right)
  }

  // ...
}

La consola revela el problema

La consola mostrándonos left before 4 y left after 4

Aunque se estableció un nuevo valor para left llamando a setLeft(left + 1), el valor anterior persiste a pesar de la actualización. Como resultado, el intento de contar las pulsaciones de botones produce un resultado demasiado pequeño:

setTotal(left + right)

La razón de esto es que una actualización de estado en React ocurre asíncronamente, es decir, no inmediatamente sino "en algún momento" antes de que el componente se renderice nuevamente.

Podemos arreglar la aplicación de la siguiente manera:

const App = () => {
  // ...
  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    const updatedLeft = left + 1
    setLeft(updatedLeft)
    setTotal(updatedLeft + right)
  }

  // ...
}

Así que ahora el número de pulsaciones de botones se basa definitivamente en el número correcto de pulsaciones del botón izquierdo.

Renderizado condicional

Modifiquemos nuestra aplicación para que el renderizado del historial de clics sea manejado por un nuevo componente History:

const History = (props) => {  if (props.allClicks.length === 0) {    return (      <div>        the app is used by pressing the buttons      </div>    )  }  return (    <div>      button press history: {props.allClicks.join(' ')}    </div>  )}
const App = () => {
  // ...

  return (
    <div>
      {left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {right}
      <History allClicks={allClicks} />    </div>
  )
}

Ahora el comportamiento del componente depende de si se ha hecho clic en cualquier botón. Si no es así, lo que significa que el array allClicks está vacío, el componente muestra un elemento div con algunas instrucciones en su lugar:

<div>the app is used by pressing the buttons</div>

Y en todos los demás casos, el componente muestra el historial de clics:

<div>
  button press history: {props.allClicks.join(' ')}
</div>

El componente History representa elementos React completamente diferentes según el estado de la aplicación. Esto se llama renderizado condicional.

React también ofrece muchas otras formas de hacer renderizado condicional. Veremos esto más de cerca en la parte 2.

Hagamos una última modificación a nuestra aplicación, para usar el componente Button que definimos anteriormente:

const History = (props) => {
  if (props.allClicks.length === 0) {
    return (
      <div>
        the app is used by pressing the buttons
      </div>
    )
  }

  return (
    <div>
      button press history: {props.allClicks.join(' ')}
    </div>
  )
}

const Button = ({ handleClick, text }) => (  <button onClick={handleClick}>    {text}  </button>)
const App = () => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])

  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    setLeft(left + 1)
  }

  const handleRightClick = () => {
    setAll(allClicks.concat('R'))
    setRight(right + 1)
  }

  return (
    <div>
      {left}
      <Button handleClick={handleLeftClick} text='left' />      <Button handleClick={handleRightClick} text='right' />      {right}
      <History allClicks={allClicks} />
    </div>
  )
}

React Antiguo

En este curso usamos el state hook para agregar estado a nuestros componentes de React, que es parte de las versiones más nuevas de React y está disponible desde la versión 16.8.0 en adelante. Antes de la adición de hooks, no había forma de agregar estado a los componentes funcionales. Los componentes que requerían el estado tenían que definirse como componentes de clase, utilizando la sintaxis de clase de JavaScript.

En este curso hemos tomado la decisión un poco radical de utilizar hooks exclusivamente desde el primer día, para asegurarnos de que estamos aprendiendo las actuales y futuras versiones de React. Aunque los componentes funcionales son el futuro de React, sigue siendo importante aprender la sintaxis de clase, ya que hay miles de millones de líneas de código antiguo de React que podrías terminar manteniendo algún día. Lo mismo se aplica a la documentación y los ejemplos de React con los que puedes tropezar en Internet.

Aprenderemos más sobre los componentes de clase de React más adelante en el curso.

Depuración de aplicaciones React

Una gran parte del tiempo de un desarrollador típico se dedica a depurar y leer el código existente. De vez en cuando podemos escribir una línea o dos de código nuevo, pero una gran parte de nuestro tiempo se dedica a tratar de averiguar por qué algo está roto o cómo funciona algo. Las buenas prácticas y herramientas para depurar son extremadamente importantes por este motivo.

Por suerte para nosotros, React es una librería extremadamente amigable para los desarrolladores cuando se trata de depurar.

Antes de continuar, recordemos una de las reglas más importantes del desarrollo web.

La primera regla de desarrollo web

Mantén la consola de desarrollador del navegador abierta en todo momento.

La Consola, en particular, debería estar siempre abierta, a menos que haya una razón específica para ver otra pestaña.

Mantén tu código y la página web abiertos juntos al mismo tiempo, todo el tiempo.

Si tu código falla al compilarse y tu navegador se ilumina como un árbol de Navidad:

Captura de pantalla de un error, apuntando a la linea de código en donde se ha generado

no escribas más código, sino busca y soluciona el problema inmediatamente. Aún no ha habido un momento en la historia de la codificación en el que el código que no se compila comience a funcionar milagrosamente después de escribir grandes cantidades de código adicional. Dudo mucho que tal evento ocurra durante este curso.

La depuración de la vieja escuela basada en impresión siempre es una buena idea. Si el componente

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

no funciona como se esperaba, es útil comenzar a imprimir sus variables en la consola. Para hacer esto de manera efectiva, debemos transformar nuestra función en la forma menos compacta y recibir el objeto props completo sin desestructurarlo inmediatamente:

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

Esto revelará inmediatamente si, por ejemplo, uno de los atributos se ha escrito mal al usar el componente.

Nota: Cuando use console.log para depurar, no combines objetos como en Java utilizando el operador de adición:

console.log('props value is ' + props)

Si haces eso, terminará mostrándote un mensaje poco informativo:

props value is [Object object]

En su lugar, separa las cosas que deseas imprimir en la consola con una coma:

console.log('props value is', props)

De esta forma, los elementos separados por una coma estarán disponibles en la consola del navegador para una inspección más detallada.

Imprimir en la consola no es de ninguna manera la única forma de depurar nuestras aplicaciones. Puedes pausar la ejecución del código de tu aplicación en el depurador de la consola de desarrollador de Chrome, escribiendo el comando debugger en cualquier parte de tu código.

La ejecución se detendrá una vez que llegue a un punto donde se ejecuta el comando debugger:

debugger pausado en dev tools

Al ir a la pestaña Console (consola), Es fácil inspeccionar el estado actual de las variables:

screenshot de la consola

Una vez que se descubre la causa del error, puedes eliminar el comando debugger y actualizar la página.

El depurador también nos permite ejecutar nuestro código línea por línea con los controles que se encuentran en el lado derecho de la pestaña Sources.

También puedes acceder al depurador sin el comando debugger agregando puntos de interrupción en la pestaña Sources. La inspección de los valores de las variables del componente se puede hacer en la sección Scope:

ejemplo de breakpoint en devtools

Es muy recomendable instalar la extensión React developer tools para Chrome. Agregará una nueva pestaña Components a las herramientas de desarrollo. La nueva pestaña de herramientas de desarrollador se puede usar para inspeccionar los diferentes elementos de React en la aplicación, junto con su estado y props:

screenshot de herramientas de desarrollo de react

El estado del componente App se define así:

const [left, setLeft] = useState(0)
const [right, setRight] = useState(0)
const [allClicks, setAll] = useState([])

Las herramientas de desarrollo muestran el estado de los hooks en el orden de su definición:

estado de hooks en react dev tools

El primer State contiene el valor del estado left, el siguiente contiene el valor del estado right y el último contiene el valor del estado allClicks.

Reglas de los Hooks

Hay algunas limitaciones y reglas que debemos seguir para asegurarnos de que nuestra aplicación utilice correctamente las funciones de estado basadas en hooks.

La función useState (así como la función useEffect presentada más adelante en el curso) no se debe llamar desde dentro de un loop, una expresión condicional o cualquier lugar que no sea una función que defina a un componente. Esto debe hacerse para garantizar que los hooks siempre se llamen en el mismo orden o, si este no es el caso, la aplicación se comportará de manera errática.

En resumen, los hooks solo se pueden llamar desde el interior de un cuerpo de la función que define un componente de React:

const App = () => {
  // estos están bien
  const [age, setAge] = useState(0)
  const [name, setName] = useState('Juha Tauriainen')

  if ( age > 10 ) {
    // esto no funciona!
    const [foobar, setFoobar] = useState(null)
  }

  for ( let i = 0; i < age; i++ ) {
    // esto tampoco está bien
    const [rightWay, setRightWay] = useState(false)
  }

  const notGood = () => {
    // y esto también es ilegal
    const [x, setX] = useState(-1000)
  }

  return (
    //...
  )
}

Revision de los Controladores de Eventos

El control de eventos ha demostrado ser un tema difícil en iteraciones anteriores de este curso.

Por esta razón volveremos a tratar el tema.

Supongamos que estamos desarrollando esta sencilla aplicación con el siguiente componente App:

const App = () => {
  const [value, setValue] = useState(10)

  return (
    <div>
      {value}
      <button>reset to zero</button>
    </div>
  )
}

Queremos hacer clic en el botón para restablecer el estado almacenado en la variable value.

Para que el botón reaccione a un evento de clic, tenemos que agregarle un controlador de eventos.

Los controladores de eventos siempre deben ser una función o una referencia a una función. El botón no funcionará si el controlador de eventos se establece en una variable de cualquier otro tipo.

Si definiéramos el controlador de eventos como un string:

<button onClick="crap...">button</button>

React nos advertiría sobre esto en la consola:

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `string` type.
    in button (at index.js:20)
    in div (at index.js:18)
    in App (at index.js:27)

El siguiente intento tampoco funcionaría:

<button onClick={value + 1}>button</button>

Hemos intentado establecer el controlador de eventos en value + 1 que simplemente devuelve el resultado de la operación. React nos advertirá amablemente sobre esto en la consola:

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `number` type.

Este intento tampoco funcionaría:

<button onClick={value = 0}>button</button>

El controlador de eventos no es una función sino una asignación de variable, y React volverá a emitir una advertencia para la consola. Este intento también tiene fallas en el sentido de que nunca debemos mutar el estado directamente en React.

¿Qué pasa con lo siguiente?:

<button onClick={console.log('clicked the button')}>
  button
</button>

El mensaje se imprime en la consola una vez cuando se renderiza el componente, pero no sucede nada cuando hacemos clic en el botón. ¿Por qué esto no funciona incluso cuando nuestro controlador de eventos contiene una función console.log?

El problema aquí es que nuestro controlador de eventos está definido como una llamada a una función, lo que significa que al controlador de eventos se le asigna realmente el valor devuelto por la función, que en el caso de console.log es undefined.

La llamada a la función console.log se ejecuta cuando se renderiza el componente y por esta razón se imprime una vez en la consola.

El siguiente intento también tiene fallas:

<button onClick={setValue(0)}>button</button>

Una vez más, hemos intentado establecer una llamada a una función como controlador de eventos. Esto no funciona. Este intento en particular también causa otro problema. Cuando se renderiza el componente, se ejecuta la función setValue(0), lo que a su vez hace que el componente se vuelva a renderizar. La renderización a su vez llama a setValue(0) de nuevo, lo que da como resultado una recursion infinita.

La ejecución de una llamada de función en particular cuando se hace clic en el botón se puede lograr así:

<button onClick={() => console.log('clicked the button')}>
  button
</button>

Ahora el controlador de eventos es una función definida con la sintaxis de función de flecha () => console.log('clicked the button'). Cuando el componente se renderiza, no se llama a ninguna función y solo la referencia a la función de flecha se establece en el controlador de eventos. La llamada a la función ocurre solo una vez que se hace clic en el botón.

Podemos implementar el reseteo del estado en nuestra aplicación con esta misma técnica:

<button onClick={() => setValue(0)}>button</button>

El controlador de eventos ahora es la función () => setValue (0).

Definir controladores de eventos directamente en el atributo del botón no es necesariamente la mejor idea.

A menudo verás los controladores de eventos definidos en un lugar separado. En la siguiente versión de nuestra aplicación, definimos una función que luego se asigna a la variable handleClick en el cuerpo de la función del componente:

const App = () => {
  const [value, setValue] = useState(10)

  const handleClick = () =>
    console.log('clicked the button')

  return (
    <div>
      {value}
      <button onClick={handleClick}>button</button>
    </div>
  )
}

La variable handleClick ahora está asignada a una referencia de una función. La referencia se pasa al botón como el atributo onClick:

<button onClick={handleClick}>button</button>

Naturalmente, nuestra función de controlador de eventos puede estar compuesta por varios comandos. En estos casos, usamos la sintaxis de llaves más largas para las funciones de flecha:

const App = () => {
  const [value, setValue] = useState(10)

  const handleClick = () => {    console.log('clicked the button')    setValue(0)  }
  return (
    <div>
      {value}
      <button onClick={handleClick}>button</button>
    </div>
  )
}

Una función que devuelve una función

Otra forma de definir un controlador de eventos es utilizar una función que devuelve una función.

Probablemente no necesites utilizar funciones que devuelvan funciones en ninguno de los ejercicios de este curso. Si el tema parece particularmente confuso, puedes omitir esta sección por ahora y volver a ella más tarde.

Hagamos los siguientes cambios en nuestro código:

const App = () => {
  const [value, setValue] = useState(10)

  const hello = () => {    const handler = () => console.log('hello world')    return handler  }
  return (
    <div>
      {value}
      <button onClick={hello()}>button</button>
    </div>
  )
}

El código funciona correctamente aunque parece complicado.

El controlador de eventos ahora está configurado para una llamada de función:

<button onClick={hello()}>button</button>

Anteriormente dijimos que un controlador de eventos puede no ser una llamada a una función, y que tiene que ser una función o una referencia a una función. Entonces, ¿por qué funciona una llamada a función en este caso?

Cuando se renderiza el componente, se ejecuta la siguiente función:

const hello = () => {
  const handler = () => console.log('hello world')

  return handler
}

El valor de retorno de la función es otra función que se asigna a la variable handler.

Cuando React renderiza la línea:

<button onClick={hello()}>button</button>

Asigna el valor de retorno de hello() al atributo onClick. Básicamente, la línea se transforma en:

<button onClick={() => console.log('hello world')}>
  button
</button>

Dado que la función hello devuelve una función, el controlador de eventos ahora es una función.

¿Qué sentido tiene este concepto?

Cambiemos el código un poquito:

const App = () => {
  const [value, setValue] = useState(10)

  const hello = (who) => {    const handler = () => {      console.log('hello', who)    }    return handler  }
  return (
    <div>
      {value}
      <button onClick={hello('world')}>button</button>      <button onClick={hello('react')}>button</button>      <button onClick={hello('function')}>button</button>    </div>
  )
}

Ahora la aplicación tiene tres botones con controladores de eventos definidos por la función hello que acepta un parámetro.

El primer botón se define como

<button onClick={hello('world')}>button</button>

El controlador de eventos se crea ejecutando la llamada de función hello('world'). La llamada a la función devuelve la función:

() => {
  console.log('hello', 'world')
}

El segundo botón se define como:

<button onClick={hello('react')}>button</button>

La llamada de función hello('react') que crea el controlador de eventos devuelve:

() => {
  console.log('hello', 'react')
}

Ambos botones tienen sus propios controladores de eventos individualizados.

Las funciones que devuelven funciones se pueden utilizar para definir funciones genéricas que se pueden personalizar con parámetros. La función hello que crea los controladores de eventos se puede considerar como una fábrica que produce controladores de eventos personalizados destinados a saludar a los usuarios.

Nuestra definición actual es un poco verbosa:

const hello = (who) => {
  const handler = () => {
    console.log('hello', who)
  }

  return handler
}

Eliminemos las variables auxiliares y devolvamos directamente la función creada:

const hello = (who) => {
  return () => {
    console.log('hello', who)
  }
}

Dado que nuestra función hello se compone de un solo comando de retorno, podemos omitir las llaves y usar la sintaxis más compacta para las funciones de flecha:

const hello = (who) =>
  () => {
    console.log('hello', who)
  }

Por último, escribamos todas las flechas en la misma línea:

const hello = (who) => () => {
  console.log('hello', who)
}

Podemos usar el mismo truco para definir controladores de eventos que establecen el estado del componente en un valor dado. Hagamos los siguientes cambios en nuestro código:

const App = () => {
  const [value, setValue] = useState(10)

  const setToValue = (newValue) => () => {    console.log('value now', newValue)  // imprime el nuevo valor en la consola    setValue(newValue)  }
  return (
    <div>
      {value}
      <button onClick={setToValue(1000)}>thousand</button>      <button onClick={setToValue(0)}>reset</button>      <button onClick={setToValue(value + 1)}>increment</button>    </div>
  )
}

Cuando se renderiza el componente, se crea el botón thousand:

<button onClick={setToValue(1000)}>thousand</button>

El controlador de eventos se establece en el valor de retorno de setToValue(1000) que es la siguiente función:

() => {
  console.log('value now', 1000)
  setValue(1000)
}

El botón de aumento se declara de la siguiente manera:

<button onClick={setToValue(value + 1)}>increment</button>

El controlador de eventos es creado por la llamada de función setToValue(value + 1) que recibe como parámetro el valor actual de la variable de estado value aumentado en uno. Si el valor de value fuera 10, entonces el controlador de eventos creado sería la función:

() => {
  console.log('value now', 11)
  setValue(11)
}

No es necesario utilizar funciones que devuelvan funciones para lograr esta funcionalidad. Regresemos la función setToValue que es responsable de actualizar el estado, a una función normal:

const App = () => {
  const [value, setValue] = useState(10)

  const setToValue = (newValue) => {
    console.log('value now', newValue)
    setValue(newValue)
  }

  return (
    <div>
      {value}
      <button onClick={() => setToValue(1000)}>
        thousand
      </button>
      <button onClick={() => setToValue(0)}>
        reset
      </button>
      <button onClick={() => setToValue(value + 1)}>
        increment
      </button>
    </div>
  )
}

Ahora podemos definir el controlador de eventos como una función que llama a la función setToValue con un parámetro apropiado. El controlador de eventos para resetear el estado de la aplicación sería:

<button onClick={() => setToValue(0)}>reset</button>

Elegir entre las dos formas presentadas de definir tus controladores de eventos es sobre todo una cuestión de gustos.

Pasando controladores de eventos a componentes hijos

Extraigamos el botón en su propio componente:

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

El componente obtiene la función de controlador de eventos de la propiedad handleClick, y el texto del botón de la propiedad text. Usemos el nuevo componente:

const App = (props) => {
  // ...
  return (
    <div>
      {value}
      <Button handleClick={() => setToValue(1000)} text="thousand" />      <Button handleClick={() => setToValue(0)} text="reset" />      <Button handleClick={() => setToValue(value + 1)} text="increment" />    </div>
  )
}

Usar el componente Button es simple, aunque debemos asegurarnos de que usamos los nombres de atributos correctos al pasar props al componente.

Captura de pantalla del uso correcto de los nombres de atributos

No definir componentes dentro de los componentes

Empecemos a mostrar el valor de la aplicación en su propio componente Display.

Cambiaremos la aplicación definiendo un nuevo componente dentro del componente App.

// Este es lugar correcto para definir un componente
const Button = (props) => (
  <button onClick={props.handleClick}>
    {props.text}
  </button>
)

const App = () => {
  const [value, setValue] = useState(10)

  const setToValue = newValue => {
    console.log('value now', newValue)
    setValue(newValue)
  }

  // No definas componentes adentro de otro componente
  const Display = props => <div>{props.value}</div>
  return (
    <div>
      <Display value={value} />
      <Button handleClick={() => setToValue(1000)} text="thousand" />
      <Button handleClick={() => setToValue(0)} text="reset" />
      <Button handleClick={() => setToValue(value + 1)} text="increment" />
    </div>
  )
}

La aplicación todavía parece funcionar, pero ¡no implementes componentes como este! Nunca definas componentes dentro de otros componentes. El método no proporciona beneficios y da lugar a muchos problemas desagradables. Los mayores problemas se deben al hecho de que React trata un componente definido dentro de otro componente como un nuevo componente en cada render. Esto hace imposible que React optimice el componente.

En su lugar, movamos la función del componente Display a su lugar correcto, que está fuera de la función del componente App:

const Display = props => <div>{props.value}</div>

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

const App = () => {
  const [value, setValue] = useState(10)

  const setToValue = newValue => {
    console.log('value now', newValue)
    setValue(newValue)
  }

  return (
    <div>
      <Display value={value} />
      <Button handleClick={() => setToValue(1000)} text="thousand" />
      <Button handleClick={() => setToValue(0)} text="reset" />
      <Button handleClick={() => setToValue(value + 1)} text="increment" />
    </div>
  )
}

Lectura útil

Internet está lleno de material relacionado con React. Sin embargo, utilizamos un estilo de React tan nuevo que una gran mayoría del material que se encuentra en línea está desactualizado para nuestros propósitos.

Puedes encontrar útiles los siguientes enlaces:

  • Vale la pena echarle un vistazo a la documentación oficial de React en algún momento, aunque la mayor parte será relevante solo más adelante en el curso. Además, todo lo relacionado con los componentes basados en clases es irrelevante para nosotros;
  • Algunos cursos en Egghead.io como Start learning React son de alta calidad y la recientemente actualizada Guía para principiantes de React también es relativamente buena; Ambos cursos introducen conceptos que también se presentarán más adelante en este curso. Nota: El primero usa componentes de clase pero el segundo usa los nuevos componentes funcionales.

Juramento de los programadores web

Programar es difícil, por eso usaré todos los medios posibles para hacerlo más fácil.

  • Tendré la consola de desarrollador de mi navegador abierta todo el tiempo.
  • Progreso con pequeños pasos.
  • Escribiré muchas declaraciones console.log para asegurarme de que entiendo cómo se comporta el código y para ayudar a identificar problemas.
  • Si mi código no funciona, no escribiré más código. En lugar de eso, empiezo a eliminar el código hasta que funcione o simplemente vuelvo a un estado en el que todo seguía funcionando.
  • Cuando pido ayuda en el canal de Discord o Telegram del curso o en otro lugar, formulo mis preguntas correctamente, consulta aquí como pedir ayuda.

Utilización de grandes modelos de lenguaje (LLM)

Modelos de lenguaje grandes como ChatGPT, Claude y GitHub Copilot han demostrado ser muy útiles en el desarrollo de software.

Personalmente, principalmente uso Copilot, el cual se integra a la perfección con VS Code gracias al plugin.

Copilot es útil en una amplia variedad de escenarios. A Copilot se le puede pedir que genere código para un archivo abierto describiendo la funcionalidad deseada en texto:

input de copilot en vscode

Si el código parece bueno, Copilot lo añade al archivo:

código agregado por copilot

En el caso de nuestro ejemplo, Copilot solo creó un botón, el controlador de eventos handleResetClick no está definido.

También se puede generar un controlador de eventos. Al escribir la primera línea de la función, Copilot ofrece la funcionalidad a generar:

sugerencia de código de copilot

En la ventana de chat de Copilot, es posible pedir una explicación de la función del área de código seleccionada:

copilot explicando como funciona el código seleccionado en la ventana de chat

Copilot también es útil en situaciones de error, copiando el mensaje de error en el chat de Copilot, obtendrás una explicación del problema y una solución sugerida:

copilot explicando el error y sugiriendo una solución

El chat de Copilot también permite la creación de un conjunto más grande de funcionalidades

copilot creando un componente de login a demanda

El grado de utilidad de las sugerencias proporcionadas por Copilot y otros modelos de lenguaje varía. Quizás el problema más grande con los modelos de lenguaje es la alucinación, a veces generan respuestas que parecen completamente convincentes, pero que, sin embargo, son completamente incorrectas. Al programar, por supuesto, el código alucinado a menudo se detecta rápidamente si el código no funciona. Situaciones más problemáticas son aquellas donde el código generado por el modelo de lenguaje parece funcionar, pero contiene errores más difíciles de detectar o, por ejemplo, vulnerabilidades de seguridad.

Otro problema al aplicar modelos de lenguaje al desarrollo de software es que es difícil para los modelos de lenguaje "entender" proyectos más grandes, y, por ejemplo, generar funcionalidad que requeriría cambios en varios archivos. Los modelos de lenguaje también son actualmente incapaces de generalizar código, es decir, si el código tiene, por ejemplo, funciones o componentes existentes que el modelo de lenguaje podría usar con cambios menores para la funcionalidad solicitada, el modelo de lenguaje no se adaptará a esto. El resultado de esto puede ser que la base de código se deteriore, ya que los modelos de lenguaje generan mucha repetición en el código, ver más por ejemplo aquí.

Al usar modelos de lenguaje, la responsabilidad siempre queda con el programador.

El rápido desarrollo de los modelos de lenguaje pone al estudiante de programación en una posición desafiante: ¿vale la pena y es incluso necesario aprender a programar a un nivel detallado, cuando casi todo se puede obtener ya hecho de los modelos de lenguaje?

En este punto, vale la pena recordar la antigua sabiduría de Brian Kernighan, el desarrollador del lenguaje de programación C:

Todo el mundo sabe que depurar es dos veces más difícil que escribir un programa en primer lugar. Entonces, si eres tan inteligente como puedes ser cuando lo escribes, ¿cómo podrás depurarlo? - Brian Kernighan

En otras palabras, dado que depurar es dos veces más difícil que programar, no vale la pena programar tal código que apenas puedes entender. ¿Cómo puede ser posible la depuración en una situación donde la programación se externaliza a un modelo de lenguaje y el desarrollador de software no entiende el código depurado en absoluto?

Hasta ahora, el desarrollo de modelos de lenguaje e inteligencia artificial aún está en una etapa donde no son autosuficientes, y los problemas más difíciles quedan para que los humanos los resuelvan. Por esto, incluso los desarrolladores de software novatos deben aprender a programar realmente bien por si acaso. Puede ser que, a pesar del desarrollo de modelos de lenguaje, se necesite aún más conocimiento en profundidad. La inteligencia artificial hace las cosas fáciles, pero se necesita un humano para resolver los líos más complicados causados por la IA. GitHub Copilot es un producto muy bien nombrado, es Copilot, un segundo piloto que ayuda al piloto principal en una aeronave. El programador sigue siendo el piloto principal, el capitán y lleva la máxima responsabilidad.

Puede ser de tu interés que desactives Copilot por defecto cuando hagas este curso y confíes en él solo en una emergencia real.