Guía de hooks React 18

0
99
Portada Guía de hooks React 18

React hooks, todo el mundo habla de ellos, los utilizan, se crean sus propios hooks personalizados, etc.

En esta guía veremos los React hooks más relevantes que existen actualmente en la versión 18 de React, desde los más comunes hasta los menos y también veremos como crear nuestro propio hook personalizado (custom hook).

Índice

  1. ¿Qué son los hooks?
  2. Hooks más comunes
  3. Hooks menos comunes
  4. Custom Hooks
  5. Conclusiones

¿Qué son los hooks?

Los hooks son funcionalidades que nos van a permitir añadir más funcionalidad a nuestros componentes. Podemos consumir los que React nos provee o también crear nuestros propios hooks.

Hay muchos tipos de hooks, algunos nos servirá para darle estados a nuestros componentes, otros para acceder a nuestros contextos, acceder a referencias del DOM, optimizaciones, etc.

Como convenio en React los hooks empiezan siempre por la palabra use ya sean los que consumimos de la propia o si nos creamos los nuestros deberían empezar así.

Hooks más comunes

useState

Documentación oficial useState

Nos permitirá crearle una variable de estado a nuestro componente. De esta manera dejará de ser un componente stateless y pasará a ser statefull, ya que tendrá un estado propio.

Este hook nos devolverá un array de 2 posiciones donde la primera es el valor del estado y la segunda posición es una función para cambiar el valor del estado.

import { useState } from "react"

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

  const increment = () => setCounter(counter + 1)
  const decrement = () => setCounter(counter - 1)

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{counter}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

Como hemos visto en el ejemplo anterior, hacemos un set del valor del countador pasándole el nuevo valor, pero hay un problema usándolo de esta forma.

Supongamos el caso de que queremos ejecutar varias veces el set del incremento para que sume 3. Tendremos una función que, al ejecutarse, ejecute tres veces el setCounter sumando 1 al contador.

const incremenet3 = () => {
    increment()
    increment()
    increment()
}

¿Cuál es el problema de esto?

Esto no sumará de 3 al valor del contador, ya que con palabras de la propia documentación: “Esto se debe a que llamar a la función set no actualiza la variable de estado en el código que ya se está ejecutando”. Por lo que solo está detectando el primer valor.

Ejemplo:

Si tenemos el contador con valor de estado 1 y ejecutamos esta función, cogerá ese valor 1 y será el que ejecute en todos los casos y cambiaría el valor a 2.

Para solucionar esto, la función set del useState puede recibir una función en la cual el parámetro será el estado previo. De esta manera se crearán unas colas las funciones de actualización de estado y podrá saber cuál era el valor anterior y modificarlo.

En nuestro caso con el contador podríamos modificarlo para que la función de incremento quede de la siguiente forma:

const increment = () => setCounter((prevCount) => prevCount + 1)

Ahora, si ejecutásemos nuestra función para sumar 3, crearía una cola por cada función de actualización y teniendo en cuenta el valor de la anterior.

useEffect

Documentación oficial useEffect

Nos permite sincronizar un componente con un sistema externo. Estos sistemas externos podrían ser diferentes, como conexión a una API, suscripción a eventos, etc.

Este hook recibirá 2 parámetros, el primero una función callback y el segundo será un array de dependencias. Este array de dependencias servirá para ejecutar el useEffect en caso de que la dependencia haya cambiado. Si ponemos un array vacío se ejecutará en el primer render nada más a la hora de montarse.

El caso más común es para hacer peticiones a una API, como el caso que vemos a continuación, donde tendremos un estado para tener los posts y luego en el useEffect haremos la llamada para obtenerlos y setear el estado.

import React, { useState, useEffect } from 'react'

const PostList = () => {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    getPosts()
      .then((data: Post[]) => {
        setPosts(data)
      })
      .catch(error => {
        console.error('Error fetching data:', error)
      })
  }, [])

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  )
}

Ejemplo con array de dependencias

Imaginemos que queremos mostrar un mensaje cada vez que el valor de una variable count cambie. Tendremos como dependencia el estado count y cada vez que cambie, cambiará el mensaje.

import { useState, useEffect } from 'react'

const CounterComponent = () => {
  const [count, setCount] = useState(0)
  const [message, setMessage] = useState('')

  useEffect(() => {
    setMessage(`El contador ahora es: ${count}`)
  }, [count])

  const increment = () => {
    setCount(prevCount => prevCount + 1)
  }

  return (
    <div>
      <p>Count: {count}</p>
      <p>{message}</p>
      <button onClick={increment}>Increment</button>
    </div>
  )
}

Quizás no te hace falta usar useEffect

Uno de los grandes problemas que hay es que mucha gente usa este hook cuando no es necesario. Un caso muy común es cuando tienes un componente, recibes una prop y quieres hacer algo y quieres hacer algo con esta prop y te creas un nuevo estado para el valor nuevo y un useEffect con la dependencia de la prop para que se haga este cálculo cada vez que cambia.

Esto es innecesario porque cada vez que nuestra prop cambia, se va a renderizar de nuevo el componente.

Un ejemplo claro es el de la propia documentación de React. Recibimos una lista de todos y un filtro y queremos mostrar en nuestra interfaz los todos con el filtro aplicado.

Vemos como en el primer ejemplo (mal hecho) crea un nuevo estado y un useEffect como habíamos comentado anteriormente.

const TodoList = ({ todos, filter }) => {
  const [newTodo, setNewTodo] = useState('')

  const [visibleTodos, setVisibleTodos] = useState([])
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter))
  }, [todos, filter])

  // ...
}

Esto provocará más renderizados del componente y también será menos óptimo, ya que estamos usando otro estado más y un useEffect que no necesitamos.

Entendiendo cómo funciona React, cuando las props cambien, el componente se renderizará de nuevo como comenta anteriormente. Así que no nos hará falta ningún useEffect y nos quedaría un código como el siguiente.

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('')

  const visibleTodos = getFilteredTodos(todos, filter)
  // ...
}

Cada vez que cambie una prop se ejecutará el filtro sobre los todos que hemos recibido. Si cambia el valor de la prop volverá a ejecutar este código.

useContext

Documentación oficial useContext

Permite acceder a un contexto que hayamos creado desde nuestro componente.

Imaginemos que tenemos un contexto del tema de nuestra aplicación que puede ser un tema claro u oscuro. Con este componente podríamos acceder a ese contexto que hemos creado y sus propiedades.

En el siguiente ejemplo se ve como desde un componente podemos acceder al contexto del tema y este nos proveerá del propio valor para saber si es claro u oscuro y también de la función para poder cambiar este valor.

import { useContext } from "react"
import { ThemeContext } from "../context/ThemeContext"

export const Component = () => {
  const {theme, toggleTheme} = useContext(ThemeContext)

  return (
    <div>
      <button onClick={toggleTheme}>Toggle Theme </button>
       <div style={{backgroundColor: theme === 'light' ? "#F2F8FE" : "black"}}>
        {theme}
      </div>
    </div>
  )
}

useRef

Documentación oficial useRef

Nos permite hacer referencia a un valor que no es necesario para renderizar. Pueden ser valores o incluso referencias al DOM.

En este ejemplo vemos cómo creamos una referencia al principio usando el hook y luego la asociamos con el elemento input.

Teniendo esta referencia al input, podemos acceder a sus propiedades, como puede ser este caso para hacer un focus o incluso podríamos acceder al valor del input accediendo a inputRef.current.value.

import { useRef } from 'react'

const TextInput = () => {
  const inputRef = useRef(null)

  const handleClick = () => {
    inputRef.current.focus()
  };

  return (
    <div>
      <input ref={inputRef} type="text" /> 
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
};

Tener en cuenta que cuando se modifica el valor de una referencia no se renderiza de nuevo el componente. Su comportamiento es diferente a los estados en React.

https://react.dev/learn/referencing-values-with-refs#differences-between-refs-and-state

useMemo

Documentación oficial useMemo

Nos permitirá cachear el resultado de un cálculo entre re-renders. Optimizaremos la carga de este componente donde estemos haciendo un cálculo costoso.

Este hook recibirá 2 parámetros, el primero será una callback que nos devuelva un valor para poder cachear y el segundo un array de dependencias para que se ejecute si una de estas cambia.

Supongamos que tenemos un componente que nos mostrará el cálculo del factorial de un número y tenemos nuestra función del cálculo separada.

Usando el useMemo podremos cachear este resultado del cálculo y que dependa del número que pongamos gracias al array de dependencias que recibe este hook. De esta manera, siempre que cambiemos el número, se hará el cálculo de nuevo, pero si fuera un número cuyo resultado está cacheado, no tendrá que volver a hacer el cálculo.

export const FactorialCalculator = ({ number }) => {
  const [value, setValue] = useState(number)

  const factorial = useMemo(() => calculateFactorial(value), [value])

  return (
    <div>
      <p>Enter a number:</p>
      <input value={value} onChange={e => setValue(parseInt(e.target.value) || 0)} />
      <p>The factorial of {value} is {factorial}</p>
    </div>
  );
}

useCallback

Documentación oficial useCallback

Nos permitirá cachear una función entre re-renders

> IMPORTANTE: diferenciar con useMemo. En este caso se cachea la función, no el resultado de la función.

En el siguiente ejemplo tenemos una página principal donde se mostrarán por un lado una lista de todos y por otro lados unos usuarios. El problema en este caso si no usamos el useCallback es que cuando cambie el estado de los usuarios se volverá a renderizar todos incluido el componente Todos y esto hará que se vuelva a crear la función addTodo que le estamos pasando aunque no haya cambiado ningún valor de su dependencia.

Esto lo arreglamos usando useCallback de manera que cacheará la función (no el resultado) y solo se volverá a crear la función en caso de que hayan cambiado los todos. Esto hará que sea mucho más óptimo porque no tiene que estar creando funciones nuevas en cada renderizado.

export const HomePage = () => {
  const [todos, setTodos] = useState([])
  const [users, setUsers] = useState([])
  //...
  const addTodo = useCallback((newTodo) => {
    setTodos([...todos, newTodo])
  }, [todos])
  //...

  return (
    <div>
      <Todos todos={todos} addTodo={addTodo} />
      //...
      //...Users component
    </div>
  );
}

useId

Documentación oficial useId

Este hook nos permitirá generar un identificador único para usar en los atributos de accesibilidad.

IMPORTANTE: no usar para generar keys o identificadores para datos.

En vez de crear un identificador hardcoded como este caso.

import { useId } from "react"

const component = () => {
  const passwordHintId = useId()

  return (
    <div>
      <h2>useID</h2>
      <div>
        <label>
          Password:
          <input
            type="password"
            aria-describedby="password-hint"
          />
        </label>
        <p id="password-hint">
          The password should contain at least 18 characters
        </p>
      </div>
    </div>
  )
}

Lo que haremos será generar el identificador con este hook de manera que estos elementos estarán relacionados por el identificador único generado automáticamente y sin ser una cadena de texto hardcoded.

import { useId } from "react"

const component = () => {
  const passwordHintId = useId();

  return (
  <div>
     <h2>useID</h2>
     <div>
       <label>
         Password:
          <input
            type="password"
            aria-describedby={passwordHintId}
          />
       </label>
       <p id={passwordHintId}>
         The password should contain at least 18 characters
       </p>
    </div>
  </div>
  )
}

useImperativeHandle

Documentación oficial useImperativeHandle

Este hook nos permitirá personalizar lo que queremos exponer de una referencia.

Veamos un ejemplo con un componente que tendrá un input y luego un componente padre que usará este componente.

Para este caso de ejemplo le diremos que exponga el focus del input.

const ChildComponent = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focusInput: () => {
      inputRef.current.focus()
    }
  }))

  return <input ref={inputRef} type="text" /> 
})

De esta manera solo estaremos exponiendo la funcionalidad de focus de este input y no se podrá acceder a otras propiedades.

Desde el componente padre, como vemos en el siguiente código, podremos llamar a esta función que hemos creado que hace el focus. Si quisiéramos acceder a otra propiedad como el value por ejemplo, nos daría un undefined porque no la hemos expuesto.

const ParentComponent = () => {
  const childRef = useRef(null)

  const handleClick = () => {
    childRef.current.focusInput()
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
};

useReducer

Documentación oficial useReducer

Este hook nos permite añadir un reducer a nuestro componente.

Imaginemos que tenemos un contador y este tiene 2 acciones: sumar y restar un valor al estado del contador. Crearíamos nuestra función reducer como vemos ahora:

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      throw new Error('Acción no válida')
  }
};

Luego, en nuestro componente usaremos el hook useReducer al que le pasaremos nuestro reducer creado anteriormente y un valor inicial. Este nos devolverá un array con 2 valores. La primera posición con el valor del estado y la segunda un dispatch que nos permitirá ejecutar la acción que queramos del reducer.

const Counter = () => {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 })

  return (
    <div>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
    </div>
  );
}

De esta manera, tenemos centralizada la lógica de las diferentes acciones que tendrá nuestro contador. Nos permitirá escalar también comportamientos de nuestro contador, ya que si queremos introducir más operación, lo haremos directamente en el reducer que hemos creado.

Hooks menos comunes

useLayoutEffect

Documentación oficial useLayoutEffect

Este hook se ejecuta de manera síncrona después de todas las actualizaciones del DOM, pero antes de que el navegador pinte en la pantalla, lo que lo hace útil para manipular el DOM de forma síncrona y realizar tareas que requieren acceso a las dimensiones o posiciones de los elementos del DOM antes de que se rendericen.

La principal diferencia entre useEffect y useLayoutEffect es cuándo se ejecutan. El useEffect se ejecuta de manera asíncrona después de que se ha completado la renderización y el navegador se ha pintado en la pantalla. Mientras que el useLayoutEffect se ejecuta de manera síncrona después de que todas las actualizaciones del DOM se hayan completado, pero antes de que el navegador pinte en la pantalla.

En este ejemplo vemos como utilizamos useLayoutEffect para cambiar el color de fondo de la página después de que se haya actualizado el DOM, pero aún no se haya pintado.

const Component = () => {
  const [color, setColor] = useState('white')

  useLayoutEffect(() => {
    document.body.style.backgroundColor = color
  }, [color])

  const changeColor = () => {
    setColor(prevColor => prevColor === 'white' ? 'lightblue' : 'white')
  }

  return (
    <div>
      <p>Haz click para cambiar el color de fondo</p>
      <button onClick={changeColor}>Cambiar color</button>
    </div>
  );
}

Esto nos permitirá que el cambio de color sea inmediato y los usuarios vean el nuevo color de fondo sin parpadeos o retrasos notables.

useSyncExternalStore

Documentación oficial useSyncExternalStore

Nos permitirá suscribirnos a una store externa. Muchas veces en React tenemos que consumir apis de terceros que manejan el estado, la API del navegador también.

> React recomienda siempre que puedas usar useState y useReducer para manejar estados. En caso de que sea algo externo que no sea con código en React

Un ejemplo muy claro es el que pone la propia documentación donde tenemos la API del navegador y queremos saber si el usuario tiene Internet o no.

Tenemos unas funciones subscribe y getSnapshot que nos permitirá estar atentos a los cambios y poder devolver el valor de si está online o no el usuario

function getSnapshot() {
  return navigator.onLine
}

function subscribe(callback) {
  window.addEventListener('online', callback)
  window.addEventListener('offline', callback)
  return () => {
    window.removeEventListener('online', callback)
    window.removeEventListener('offline', callback)
  };
}

Luego, en nuestro componente usaremos el hook para estar atentos a estos cambios y como vemos no hará falta hacer cálculos dentro ya que nos devolverá los valores y se actualizará el componente automáticamente.

export default function ChatIndicator() {
    const isOnline = useSyncExternalStore(subscribe, getSnapshot)
    return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>
}

useDeferredValue

Documentación oficial de useDeferredValue

Este hook nos permitirá posponer la actualización de una parte de la UI.

Supongamos que queremos evitarnos mostrar un loading mientras hacemos alguna operación de fetching de datos o un cálculo. Podríamos usar este hook para que cuando se esté haciendo la operación la UI se mantiene igual y cuando ha acabado cambiará la UI con el resultado.

En este ejemplo tenemos una lista de todos y cada vez que cambie el estado se guardará en el valor deferred de manera que se quedará en la versión anterior hasta que tenga uno nuevo. Por lo que si tenemos un input que actualiza estos todos y no mostrará el nuevo hasta que lo tenga. Así el usuario siempre ve información en la pantalla y no un loading que muchas veces no queremos mostrar.

import { useState, useDeferredValue } from 'react'

function SearchPage() {
  const [todos, setTodos] = useState([])
  const deferredTodos = useDeferredValue(todos)
  // ...
}

Custom Hooks

React también nos da la capacidad de crearnos nuestros hooks personalizados para poder consumir en nuestro código. Estos podrán usar todo lo que nos permite React. Podemos devolver JSX, valores, objetos, arrays, usar otros hooks dentro del nuestro, etc.

Como convención, se usa la palabra use al principio también para saber que es un hook y no una función normal.

Usar custom hooks nos puede beneficiar a la hora de tener un código más limpio, desacoplado, fácil de cambiar, etc.

Si vamos a un ejemplo sencillo como puede ser un contador, podríamos sacar toda la lógica de este a un custom hook.

import { useState } from 'react'

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

  const increment = () => setCount(prevCount => prevCount + 1)
  const decrement = () => setCount(prevCount => prevCount - 1)
  const reset = () => setCount(initialValue)

  return { count, increment, decrement, reset }
}

export useCounter

Ahora tenemos un custom hook que recibirá un valor inicial para el contador y nos devolverá el valor del contador y las diferentes acciones que podemos hacer con él. Por lo que ahora en nuestro componente nos quedará un código mucho más limpio y con la lógica separada.

import useCounter from './useCounter'

const Counter = () => {
  const { count, increment, decrement, reset } = useCounter(0)

  return (
    <div>
      <button onClick={decrement}>Decrement</button>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

Conclusiones

Después de dar un repaso por los hooks que pueden ser más importantes podemos tener claro que actualmente si trabajas en las versiones más modernas de React con componentes funcionales, el conocimiento de los hooks es imprescindible, ya que será la manera en la que puedas interactuar en el ciclo de vida de los componentes, añadir funcionalidades, optimizaciones y también reutilización de lógica con los custom hook.
Creo también que es muy importante conocer los diferentes hooks que nos proporciona React porque siempre se suelen usar 2 o 3 cuando podrías mejorar tu aplicación mucho más si usas las diferentes opciones que ofrecen en los casos necesarios.
En futuras versiones de React añadirán nuevos hooks y quitarán otros, pero está claro que la tendencia va a seguir siendo usar hooks por lo que cuanto más lo entendamos y nos acostumbremos a usarlos en el día a día, mucho mejor.
¡Muchas gracias! 🚀

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad