React Hooks Tutorial en español desde cero

React Hooks Tutorial en español desde cero
Guía paso a paso para aprender los React Hooks

¿Qué son los Hooks en React?

Los React Hooks fueron incorporados a partir de la versión 16.8 de React js y son funciones de JavaScript que nos permiten utilizar las características de esta biblioteca en componentes funcionales.

Para dar solo un ejemplo: a partir de ahora puedes declarar variables de estado en componentes funcionales, algo que antes era posible sólo en componentes de tipo clase.

import { useState } from 'react'

const Count = () => {
  const [count, setCount] = React.useState(0)

  const handleClick = () => {
    setCount(count + 1)
  }

  return (
    <>
      <p>Clicks: {count}</p>
      <button onClick={handleClick}>Clickeame</button>
    </>
  )
}

Si el código anterior te parece extraño, no te preocupes, ¡Así nos pasa a todos la primera vez! más adelante te voy a hablar más a detalle sobre este hook :D

En este mega post vamos a cubrir los aspectos esenciales y ejemplos de hooks para que puedas dominarlos rápidamente.

Contenidos


Como te dije al inicio, los hooks no son más que funciones de JavaScript que React detecta y las hace "especiales" porque permiten acceder a lo que nos encanta de esta biblioteca: el uso de estados, los re renders, manejo de ciclo de vida y más.

Manejo de estados con useState

El hook useState te permite poder usar variables de estado dentro de un componente funcional. Este es el hook que probablemente más usaras por lo que comprenderlo bien va a ser muy importante.

Hook useState vs classes

A continuación vamos a hacer un componente de tipo clase que sirva para contar los clicks que hace el usuario a un botón.

class Count extends React.Component {
  state = {
    count: 0,
  }

  handleClick = () => this.setState({ count: this.state.count + 1 })

  render() {
    const { count } = this.state
    return (
      <>
        <p>Clicks: {count}</p>
        <button onClick={this.handleClick}>Clickeame</button>
      </>
    )
  }
}

Nota que no usamos el constructor para iniciar el valor del state.

Lo definimos por fuera del constructor basados en la sintaxis alternativa, es decir, declarar el estado como una propiedad de la clase desde fuera del constructor.

Ahora vamos a hacer el ejemplo equivalente en componente funcional utilizando el hook useState.

const Count = () => {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
  }

  return (
    <>
      <p>Clicks: {count}</p>
      <button onClick={handleClick}>Clickeame</button>
    </>
  )
}

Mucho mejor! el componente es más simple, más limpio y con el mismo poder del manejo de estado de React 😄.

Nos retorna siempre un array con dos elementos.

const [count, setCount] = useState(0)

La sintaxis que vemos con los corchetes no es de React sino de ES6 y es llamada destructuración.

En la posición cero del array nos retorna la variable de estado y la posición uno nos retorna una función que nos sirve para actualizar la variable de estado.

Por lo tanto, lo siguiente es igualmente válido para Javascript.

const MyComponent = () => {
  const state = React.useState(null)
  const myState = state[0]
  const setMyState = state[1]
}

Como puedes ver, es más limpio usar la destructuración por lo que siempre veras ejemplos de ese modo.

Y al ser destructurado, podemos nombrar las dos variables de la manera que convenga mejor a nuestros intereses.

Lo recomendado es que sean nombres con significado, que indiquen cuál es su propósito.

Ya que la segunda variable es una función que actualiza el estado, en todos los ejemplos vas a encontrar que inicia con “set” y termina con el nombre de la variable de estado.

Recibe por parámetro el valor inicial de la variable de estado

A diferencia del estado en un componente de tipo clase, la variable de estado usando este hook no tiene que ser necesariamente un objeto.

Nuestro estado puede ser una variable de los siguientes tipos:

  • String.
  • Boolean.
  • Number.
  • Float.
  • Null.
  • Undefined.
  • Object.
  • Array.
const MyComponent = () => {
  const [stateVariable, setStateVariable] = React.useState(false) // <<< pasamos un booleano
}

Para valores de estado iniciales que sean resultado de un calculo costoso, puedes pasar como valor inicial una función que retorne dicho valor calculado.

const MyComponent = () => {
  const [
     stateVariable,
     setStateVariable
   ] = React.useState(() => someExpensiveCalc(); // aqui
 };

Ten en cuenta que los valores iniciales del estado se ejecutan solo una vez y es cuando se monta el componente.

Puedes declarar mas de un estado local

Va a ser común que necesites más de una variable de estado en un componente.

En lugar de tener una sola variable de estado como un objeto que contenga múltiples valores, probablemente sea mejor separar por “preocupaciones” (separation of concerns principle).

// dentro de un componente funcional

const [username, setUsername] = useState('')
const [isAdult, setIsAdult] = useState(false)
const [age, setAge] = useState(0)
const [userTags, setUserTags] = useState([])

Te recomiendo que trates de mantener tus variables de estado lo mas simples posibles y separarlas en diferentes variables cada que puedas.

Sin embargo, a veces vas a necesitar usar arrays u objetos como variables de estado, en esos casos te recomiendo que trates de mantenerlos lo mas planos posibles.

Para estructuras más complejas (un objeto de objetos, un array de objetos que tienen objetos, etc); lo recomendable es usar el hook useRedux como veremos en su post correspondiente.

Actualizando Arrays

Es importante que tengas en cuenta la manera correcta de actualizar el estado cuando usas este tipo de datos ya que puedes caer en comportamientos inesperados.

A continuación te comparto un vídeo corto donde explico paso a paso cómo hacer un CRUD de un array con useState. Da click a la siguiente imagen.

Ciclo de vida de un componente con useEffect

EL hook useEffect es el segundo hook que más vas a usar debido a que nos ayuda en todo lo relativo a ciclos de vida de un componente y efectos secundarios.

Puedes estar pensando: "¿Y qué rayos es un efecto secundario, Juanito?"

Tal como dice la documentación de React, los siguientes son ejemplos de efectos secundarios:

  • Peticiones de datos.
  • Establecimiento de suscripciones.
  • Actualizaciones manuales del DOM.

Esto ya se viene haciendo en React en los componentes de tipo clase por medio de los ciclos de vida.

Pues bien, estos efectos secundarios ahora también los podemos hacer en componentes funcionales con este react hook.

La manera en que luce el hook useEffect es la siguiente:

const MyComponent = () => {
  useEffect(() => {
    console.log('useEffect ejecutado')
  })

  return 'componente con useEffect :o'
}

Características

  • Recibe dos parámetros: el primero una función y el segundo un array cuyos valores serán variables de las que depende nuestro efecto (este array es opcional).
  • Se ejecuta en cada renderizado incluyendo el primero.
  • Puedes usar más de un useEffect dentro de un componente.
  • Está diseñado para que si la función que pasamos por parámetro retorna a su vez otra función, React va a ejecutar dicha función cuando se desmonte el componente.

Ejemplo:

const MyComponent = ({ someProp }) => {
  useEffect(() => {
    console.log('se ejecuta cada render')
    console.log('incluso el inicial')
  })

  useEffect(() => {
    console.log('se ejecuta cada vez que se actualiza el valor de someProp', someProp)
  }, [someProp])

  useEffect(() => {
    // code
    return () => console.log('componente desmontado')
  })

  return ':)'
}

Esto es lo mínimo necesario que necesitas saber para comenzar a usar useEffect.

Pero para ponerla más fácil, vamos a ver ejemplos de casos prácticos del mundo real en el uso de este hook.

¿Cómo hacer un fetch (API Call)? ciclo de vida componentDidMount con useEffect

El siguiente ejemplo va a ser sólo para fines educativos y es simular una llamada a una API.

Lo importante de este ejemplo es comparar la llamada a una api cuando se monta un componente en un componente de tipo clase y cómo es su equivalente en un componente funcional con useEffect.

Componente de tipo clase:

class PostData extends Component {
  state = {
    data: [],
  }

  componentDidMount() {
    const data = myApi.fakeFetch()
    this.setState({ data })
  }

  render() {
    return this.state.data.map(({ label }) => <p>{label}</p>)
  }
}

Nota que estamos usando el ciclo de vida componentDidMount para llamar a una api (falsa en este ejemplo) y actualizamos el estado al tener la respuesta.

Ahora vamos a hacer su equivalente con useEffect.

const SecondPostData = () => {
  const [data, setData] = useState([])

  useEffect(() => {
    const data = myApi.fakeFetch()
    setData(data)
  }, [])

  return data.map(({ label }) => <p>{label}</p>)
}

El código es menos y más simple 😃.

Este efecto se va a ejecutar sólo cuando el componente se monte la primera vez y no volverá a ejecutarse.

Recuerda que ese array de segundo parámetro sirve para indicarle a React las dependencias de ese efecto.

Si el array de dependencias está vacío, no habrá nada que lo vuelva a ejecutar, por lo que hacerlo de este modo equivale al ciclo de vida componentDidMount.

Funciones con async await en useEffect

Quizá notaste que en el ejemplo anterior no estamos manejando la api como si fuera asíncrona.

EL hook useEffect recibe una función como primer parámetro, ¿Cierto? Esa función si le colocamos async nos va a arrojar un error en consola como:

Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.

useEffect(async () => {}) // error en consola

¿Por qué nos arroja un error? porque React espera que las funciones de useEffect sean normales para garantizar su ejecución de manera correcta entre los renderizados.

Vamos a complementar el ejemplo colocando la sintaxis async await de manera correcta como se muestra a continuación:

useEffect(() => {
  const fetchData = async () => {
    const data = await myApi.fakeFetch()
    setData(data)
  }
  fetchData()
}, [])

Como puedes ver, hemos seguido la sugerencia que nos da el mensaje de error anterior y hemos colocado una función dentro del useEffect con la sintaxis async.

Moraleja: Hay que leer los mensajes de error 🤓.

Ciclo de vida componentDidUpdate con Hooks

Ahora vamos a extender el ejemplo en el escenario en el que se usa el ciclo componentDidUpdate para hacer una segunda llamada a una api cuando depende de un dato como el ID de un usuario.

Primero veamos el ejemplo de cómo sería en una componente de tipo clase usando el ciclo de vida componentDidUpdate:

class PostData extends Component {
  state = {
    data: [],
  }

  componentDidMount() {
    const { userId } = this.props
    const data = myApi.fakeFetch(userId)
    this.setState({
      data,
    })
  }

  // nuevo
  componentDidUpdate(prevProps) {
    const { userId } = this.props

    if (prevProps.userId !== userId) {
      const data = myApi.fakeFetch(userId)
      this.setState({
        data,
      })
    }
  }

  render() {
    return this.state.data.map(({ label }) => <p>{label}</p>)
  }
}

Nada fuera de lo común. Estamos haciendo una llamada a la api cada vez que el user id cambie.

Ahora veamos su equivalente en un componente funcional.

const SecondPostData = ({ userId }) => {
  const [data, setData] = useState([])

  useEffect(() => {
    const data = myApi.fakeFetch(userId)
    setData(data)
  }, [userId]) // <<< nuevo

  return data.map(({ label }) => <p>{label}</p>)
}

Sólo hemos pasado la variable userIdcomo dependencia dentro del array del useEffect.

Esto significa que nuestro efecto se va a ejecutar al montarse el componente y cada vez que la variable userIdcambie logrando así nuestro objetivo.

El código anterior lo puedes ejecutar y editar a continuación en este enlace.

Hook useEffect y componentWillUnmount

Vamos a usar de ejemplo la suscripción del evento de cuando se presiona la tecla Enter del teclado.

Ya que es un efecto secundario, iniciamos creando la suscripción dentro de un useEffect.

Cuando realizamos una suscripción por lo común vamos a necesitar “sanarla” o removerla cuando se desmonta el componente como en este ejemplo.

Para ello en nuestro useEffect donde inicializamos la suscripción vamos a retornar una función que contenga el código necesario para removerla.

const MyComponent = ({ someProp }) => {
  useEffect(() => {
    window.addEventListener('keydown', handleKeydown)

    return () => window.removeEventListener('keydown', handleKeydown)
  })

  const handleKeydown = ({ keyCode }) => {
    const enterKeyCode = 13

    if (keyCode === enterKeyCode) {
      alert('tecla Enter presionada')
    }
  }

  return 'Presiona Enter :)'
}

El mismo principio aplica para cualquier tipo de suscripciones que realices, ya sea a una API como firebase, otros eventos del DOM, etc.

Recapitulacion

En esta sección hemos visto lo siguiente:

  • El hook useEffect recibe dos parámetros: una función que React ejecutará en cada renderizado y un array de dependencias como opcional.
  • Para emular componentDidMount pasa un array vacío.
  • Para emular componentWillUnmount retorna una función con la lógica que quieres correr cuando se desmonte el componente, usado comúnmente en las suscripciones.
  • Para emular componentDidUpdate pasa las variables dependientes del estado en el array de dependencias del useEffect.
  • Puedes usar más de un useEffect en el mismo componente para separar responsabilidades en tu código.

¿Por qué deberías usar los Hooks en vez de componentes clase?

Los hooks vienen para resolver los siguientes problemas:

Rehusar lógica entre componentes: La manera de rehusar lógica antes de los hooks era por medio de patrones o técnicas como los render props y los componentes de orden superior (HOC por sus siglas en inglés).

El problema de esto es que la jerarquía de componentes tiende a terminar siendo un árbol complejo de envolvedores (wrapper hell).

Veremos en un apartado posterior cómo puedes crear tus propios hooks, de modo que ahora podrás rehusar lógica entre componentes de una manera más limpia y sin necesidad de modificar la jerarquía de los mismos.

Componentes complejos: En este contexto, un componente complejo es aquel que para funcionar termina ejecutando varias sentencias de código que pueden estar o no relacionadas en los mismos métodos de ciclos de vida de un componente.

Por ejemplo: puede que cuando se monte el componente, en el método componentDidMount, consumas una api y en ese mismo método inicialices una o varias subscripciones de eventos del navegador (o de algo diferente a la api).

Lo anterior ocasiona que tengamos lógica diferente mezclada en el mismo lugar.

El inconveniente de esto es que el código se hace difícil de leer y por lo tanto da pie a los errores humanos.

Otros Hooks

Hasta ahora hemos visto muy a detalle los ejemplos de useState y useEffect que serán los hoooks que más intensamente usarás en tu día a día.

Sin embargo, es bueno que conozcas el resto de los hooks para que los puedas tener en consideración en tus soluciones.

Listado de los hooks

Hooks de la comunidad recomendados: useForm, useSelector de redux y otros

Una estrategia que te recomiendo para aprender sobre mejores prácticas de desarrollo con los hooks, es leyendo el código fuente de los hooks más populares de la comunidad, ¡Es gratis! :D

Por ejemplo, yo mismo publiqué una biblioteca de NPM para manejar formularios en React JS hace unos años.

Así como yo, hay una gran cantidad de personas creadoras que comparten su trabajo por medio de código open source. Por ejemplo:

También te recomiendo estos repositorios que contienen una gran cantidad de ejemplos de custom hooks:

Manejo de formularios con Hooks

Manejar formularios es una de las característica que casi todas las apliciones tienen, y por lo tanto, una de las principales actividades de nuestro día a día.

En la sección anterior te mencioné que había publicado una biblioteca de NPM para manejar formularios en React, pues bien, en este post te comparto cómo fue mi proceso mental al mismo tiempo que te explico cómo manejar formularios desde cero hasta de manera avanzada con HOC y con un custom hook.

React Hooks Testing Library

¿Puedes hacer unit tests de custom hooks de manera aislada? La respuesta es: Sí. Kent C. Dodds llega al rescate de nuevo con la utilidad React Hooks Testing Library que nos permite hacer testing de manera sencilla.

Personalmente, yo prefiero hacer pruebas automatizadas en base al comportamiento y no tanto hacer unit test de custom hooks y componentes aislados.

De hecho, la misma documentación nos señala cuándo sí y cuándo no usar esta herramienta.

Úsala si:

  • Estás haciendo una biblioteca de un custom hook que no está ligado a un componente en particular.
  • El custom hook es demasiado complejo por lo que es más eficiente probarlo separado del componente.

No la uses si:

  • El custom hook es fácil de probar simplemente haciendo pruebas de un componente que lo use.
  • El custom hook es definido junto a un componente y sólo lo usará ahí.

Aquí te comparto un ejemplo extraído de la documentación:

Dado el siguiente custom hook:

import { useState, useCallback } from 'react'

export default function useCounter() {
  const [count, setCount] = useState(0)
  const increment = useCallback(() => setCount((x) => x + 1), [])
  return { count, increment }
}

Puedes crear un unit test haciendo:

import { renderHook, act } from '@testing-library/react-hooks'
import useCounter from './useCounter'

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter())

  act(() => {
    result.current.increment()
  })

  expect(result.current.count).toBe(1)
})

Lo que estamos haciendo aquí es probar que el increment realmente funcione como esperamos. En este caso, aumentar mas uno el contador actual.

Se usa el act porque nos permite simular la manera en que el hook se comportará en el navegador.

Si quieres las mejores prácticas de testing en React, te dejo unos recursos:

Errores comunes y cómo solucionarlos

La clave para poder resolver cualquier error es leer los mensajes, entender lo que nos transmite y luego copiar y pegar el mensaje en Google y hacer una búsqueda, ¡Es en serio!

En todos estos años de experiencia, aún sigo "googleando" los mensajes de error y lo seguiré haciendo.

Lo que pasa es que con el tiempo ya vas comprendiendo a identificar mejor los errores y cómo solucionarlos.

De todos modos, te presento algunos de los errores más comunes usando hooks y cómo resolverlos.

react hooks must be called in a react function component or a custom react hook function

Este error es debido a que violamos la regla de los hooks: un hook sólo puede ser llamado en el cuerpo de un componente de tipo función o del cuerpo de un custom hook.

Ejemplos:

Esto es INCORRECTO:

const MyComponent = ({ age }) => {
  const doSomething = () => {
    const [state, setState] = React.useState('carrito')
  }
  return 'esto dará un error'
}

Es incorrecto porque estamos usando useState dentro de la función doSomething.

Esto es CORRECTO:

const MyComponent = ({ age }) => {
  const [state, setState] = React.useState('carrito')

  const doSomething = () => {
    // hacer algo...
  }
  return 'works!'
}

Lo mismo aplica para cada hook o custom hook. Tienen que estar definidos en el cuerpo de la función.

react hooks useeffect has a missing dependency

Este mensaje, más que un error es un warning de Eslint.

Surge porque estamos usando useEffect y no estamos pasando correctamente el segundo parámetro opcional que es un array de dependencias.

Siempre necesitamos pasar todas las dependencias en useEffect, esto nos ayuda a evitar errores extraños que salgan por valores desactualizados al ejecutarse el callback del efecto.

Ejemplo del error:

const MyComponent = () => {
  const [state, setMySate] = React.useState(0)
  const [secondState, setSecondState] = React.useState(false)

  React.useEffect(() => {
    console.log(state, secondState)
  }, [secondState])

  return 'warning'
}

Esto es incorrecto porque no estamos pasando state en el array de dependencias.

Lo correcto es:

const MyComponent = () => {
  const [state, setMySate] = React.useState(0)
  const [secondState, setSecondState] = React.useState(false)

  React.useEffect(() => {
    console.log(state, secondState)
  }, [state, secondState])

  return 'warning'
}

Siguientes pasos para subir al siguiente nivel de seniority

Te dejo links de mis cursos premium de Udemy donde te ayudo a subir tu nivel de seniority 🤓 ☕️.

juan correa
¿Quieres pasar a nivel Senior en React?
Soy Juan Correa y he ayudado a cientos de desarrolladores a avanzar en sus carreras ¿Quieres saber cómo?