En este artículo, revelaré de manera clara y simplificada cómo React Hooks puede hacer magia en tus proyectos, tal como me hubiera gustado aprender cuando los descubrí.
En React, los hooks son utilidades que nos permiten ejecutar código arbitrario en nuestros componentes basados en acciones específicas. Los hooks proporcionan una forma de reutilizar la lógica de estado y efectos secundarios en componentes funcionales de manera más simple y declarativa.
🚨 Importante: Esta guía utiliza fragmentos de código
TypeScript
para ilustrar los conceptos. Si no estás familiarizado con TypeScript, siéntete libre de ignorar la escritura de tipos en los ejemplos.
Esta guía de referencia discutirá todos los hooks disponibles nativamente en React, pero primero, comencemos con los hooks básicos de React: useState
, useEffect
y useContext
.
Es un hook que permite a los componentes funcionales gestionar y mantener su estado interno. Puedes usarlo para declarar variables de estado y acceder a ellas en tus componentes funcionales.
La firma para el hook useState
es la siguiente:
const [state, setState] = useState(initialState)
Aquí, state
y setState
se refieren al valor del estado y a la función de actualización devuelta al invocar useState
con algún initialState
.
Es importante tener en cuenta que cuando tu componente se renderiza por primera vez e invoca useState
, el initialState
es el estado devuelto por useState
.
Además, para actualizar el estado, la función de actualización del estado setState
debe invocarse con un nuevo valor de estado, como se muestra a continuación:
setState(newValue)
Al hacer esto, se encola un nuevo renderizado del componente. useState
garantiza que el valor del state
siempre será el más reciente después de aplicar actualizaciones.
Ejemplo de uso de useState
en un componente React acerca de la tarjeta de seguimiento de X:
export function XFollowCard({ fullName, username, initialFollowing }: Props) {
const [following, setFollowing] = useState(initialFollowing) // <-- Definición
const text = following ? 'Siguiendo' : 'Seguir'
const buttonClassName = following
? 'tw-followCard-button is-following'
: 'tw-followCard-button'
const handleClick = () => {
setFollowing(!following) // Cambiando el estado
}
return (
<article className='tw-followCard'>
<header className='tw-followCard-header'>
<img
className='tw-followCard-avatar'
alt='user avatar'
src={`https://unavatar.io/${username}`}
/>
<div className='tw-followCard-info'>
<strong>{fullName}</strong>
<span className='tw-followCard-infoUserName'>@{username}</span>
</div>
</header>
<aside>
<button className={buttonClassName} onClick={handleClick}>
<span className='tw-followCard-text'>{text}</span>
<span className='tw-followCard-stopFollow'>Dejar de seguir</span>
</button>
</aside>
</article>
)
}
Es un hook de React que permite realizar efectos secundarios en componentes funcionales. Puedes usarlo para ejecutar código en respuesta a cambios en el componente, como hacer solicitudes a API, suscribirte a eventos o actualizar el DOM. useEffect
recibe una función como argumento y la ejecuta después de que el componente se renderiza o cuando ciertas dependencias cambian.
La firma básica de useEffect
es la siguiente:
useEffect(() => {
//...
})
Ejemplo de uso de useEffect
en un componente React:
const CAT_IMAGE_URL_PREFIX = 'https://cataas.com'
export function useCatImage({ fact }: { fact: string }) {
const [imageUrl, setImageUrl] = useState()
useEffect(() => {
if (!fact) return
const firstThreeWords = fact.split(' ', 3).join(' ')
fetch(
`${CAT_IMAGE_URL_PREFIX}/cat/says/${firstThreeWords}?size=50&color=red&json=true`
)
.then((res) => res.json())
.then((response) => {
const { url } = response
setImageUrl(url)
})
}, [fact])
return { imageUrl: `${CAT_IMAGE_URL_PREFIX}${imageUrl}` }
}
Algun código imperativo necesita ser limpiado para prevenir fugas de memoria. Por ejemplo, las suscripciones deben ser limpiadas, los temporizadores deben ser invalidados, etc. Para hacer esto, se retorna una función desde el callback pasado a useEffect
:
useEffect(() => {
const subscription = props.apiSubscription()
return () => {
// limpiar la suscripción
subscription.unsubscribeApi()
}
})
Se garantiza que la función de limpieza se invoque antes de que el componente sea eliminado de la interfaz de usuario.
Cuando se utiliza useEffect
en React, es crucial evitar bucles infinitos que pueden surgir de configuraciones incorrectas del efecto. Un error común es modificar el estado dentro del cuerpo del efecto de una manera que desencadene renderizaciones infinitas.
Causa común de bucles infinitos: Modificación directa del estado
useEffect(() => {
// Incorrecto: Modificar el estado dentro del cuerpo del efecto
setCounter(counter + 1)
}, [counter])
Este ejemplo provoca un bucle infinito al actualizar el estado dentro del efecto, desencadenando nuevas renderizaciones y reejecutando el efecto.
useEffect(() => {
// Correcto: Usa el callback the setCount para actualizar el estado basándo en el estado anterior.
// Esto asegura que la actualización del estado no desencadene un bucle infinito.
setCount(prevCount => prevCount + 1)
}, [count])
Evita bucles infinitos asegurándote de que las dependencias en el array de dependencias del useEffect
son estables y no cambian dentro del cuerpo del efecto. En este caso, al utilizar el callback de setCount
para actualizar el estado basándose en el estado anterior, evitamos el bucle infinito.
El problema que resuelve useContext
radica en la necesidad de pasar datos a través de la jerarquía de componentes sin recurrir al prop drilling, que es cuando se pasan props manualmente a través de varios niveles de componentes. Este enfoque puede volverse engorroso e impráctico a medida que la aplicación crece, ya que cada componente intermedio tiene que transmitir las props, generando un código menos limpio y propenso a errores.
Imagina un escenario en el que tenemos un ComponentA
que necesita pasar datos a ComponentD
. El prop drilling tradicional implicaría pasar las props a través de todos los componentes intermedios, incluso si ComponentB
y ComponentC
no necesitan esos datos.
// ComponentA.tsx
const ComponentA = ({ dataForD }: Props) => {
return <ComponentB dataForD={dataForD} />
}
// ComponentB.tsx
const ComponentB = ({ dataForD }: Props) => {
return <ComponentC dataForD={dataForD} />
}
// ComponentC.tsx
const ComponentC = ({ dataForD }: Props) => {
return <ComponentD dataForD={dataForD} />
}
// ComponentD.tsx
const ComponentD = ({ dataForD }: Props) => {
// Todo para usar el dataForD aquí
}
Este proceso, conocido como prop drilling, puede volverse inmanejable y complicado a medida que se agregan más componentes a la jerarquía.
useContext
al Rescate!useContext
simplifica en gran medida esta tarea al permitirnos crear y consumir un contexto sin pasar manualmente las props a través de cada componente. Primero, creamos un contexto usando createContext
:
// ThemeContext.ts
import { createContext } from 'react'
export const ThemeContext = createContext()
Luego, usamos el componente Provider
para envolver nuestro árbol de componentes con el valor deseado:
// App.tsx
import React from 'react'
import { ThemeContext } from './ThemeContext'
const App = () => {
return (
<ThemeContext.Provider value='dark'>
{/* Componentes anidados aquí */}
</ThemeContext.Provider>
)
}
Finalmente, en cualquier componente dentro de este contexto, podemos usar useContext
para acceder a los datos sin pasar manualmente las props:
// Button.tsx
import React, { useContext } from 'react'
import { ThemeContext } from './ThemeContext'
export const Button = () => {
const theme = useContext(ThemeContext)
return <button className={theme}>Haz clic aquí</button>
}
Ten en cuenta que el valor pasado a
useContext
debe ser el objeto de contexto, es decir, el valor devuelto al invocarReact.createContext
, noContextObject.Provider
oContextObject.Consumer
.
Esta solución evita el engorroso prop drilling y proporciona una forma más limpia y eficiente de compartir datos entre componentes en una aplicación de React. useContext
hace que la gestión de datos sea más elegante y fácil de mantener.
¡Adiós, prop drilling! 👋
Los siguientes hooks son variantes de los hooks básicos discutidos en las secciones anteriores. Si eres nuevo en los hooks, no te molestes en aprender estos por ahora. Solo son necesarios para casos específicos.
useRef
en React es un hook que facilita la creación de referencias mutables. A diferencia de useState
, useRef
no desencadena la actualización de componentes cuando su valor cambia, convirtiéndolo en una herramienta poderosa para almacenar datos que no afectan directamente la interfaz de usuario.
Así es como se utiliza el hook useRef
:
import { useRef } from 'react'
const MyComponent = () => {
const myRef = useRef(initialValue)
// ...
}
El problema que resuelve useRef
se hace evidente cuando necesitamos actualizar valores sin activar una actualización visual del componente. Para ilustrar esto, consideremos un escenario de búsqueda de películas:
export function useMovies({ search }: { search: string }) {
const [movies, setMovies] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const getMovies = async () => {
// ❌ Esto es una mala práctica porque estamos obteniendo
// las películas y volviendo a renderizar el componente
// incluso si la búsqueda es la misma que la anterior.
try {
setLoading(true)
setError(null)
const newMovies = await searchMovies({ search })
setMovies(newMovies)
} catch (error: any) {
setError(error.message)
} finally {
setLoading(false)
}
}
return { movies, getMovies, loading, error }
}
En este caso, usar useState
para el término de búsqueda provoca renderizaciones innecesarias cada vez que cambia el término de búsqueda. Aquí es donde useRef
se convierte en una solución eficiente al permitirnos actualizar search
sin afectar la interfaz de usuario:
export function useMovies({ search }: { search: string }) {
const [movies, setMovies] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const previousSearch = useRef(search) // <-- Define una ref
const getMovies = async () => {
// ✅ Si la búsqueda actual es igual a la anterior, retorna
if (previousSearch.current === search) return
try {
setLoading(true)
setError(null)
previousSearch.current = search
const newMovies = await searchMovies({ search })
setMovies(newMovies)
} catch (error: any) {
setError(error.message)
} finally {
setLoading(false)
}
}
return { movies, getMovies, loading, error }
}
Con esta implementación, useRef
ayuda a gestionar el valor de search
sin causar actualizaciones visuales no deseadas, mejorando la eficiencia y la experiencia del usuario.
El hook useMemo
en React es una herramienta poderosa para optimizar el rendimiento al memorizar el resultado de una función. Su propósito principal es evitar cálculos innecesarios, reutilizando el valor calculado anteriormente si las dependencias no han cambiado.
Así es como se utiliza el hook useMemo
:
const readTime = useMemo(() => {
const wordsPerMinute = 200
const words = text.trim().split(/\s+/).length
const value = Math.ceil(words / wordsPerMinute)
return `${value} min de lectura`
}, [text]) // solo recalcular si cambia el texto
El problema que aborda useMemo
se hace evidente cuando necesitamos realizar cálculos costosos en un componente, pero esos cálculos no son necesarios cada vez que el componente se renderiza. Tomemos el ejemplo del componente Counter
:
export function Counter({ count }: { count: number }) {
const double = count * 2 // Costoso cálculo realizado en cada renderización
return (
<div>
<p>Contador: {count}</p>
<p>Doble: {double}</p>
</div>
)
}
En este caso, el valor doble se recalcula en cada renderización, incluso si count
no ha cambiado. useMemo
resuelve esto al memorizar el resultado del cálculo solo cuando las dependencias, en este caso, la propiedad count
, han cambiado:
import { useMemo } from 'react'
export function Counter({ count }: { count: number }) {
const double = useMemo(() => count * 2, [count])
return (
<div>
<p>Contador: {count}</p>
<p>Doble: {double}</p>
</div>
)
}
Con useMemo
, si la propiedad count
no ha cambiado, se evita el recálculo del doble y se reutiliza el valor calculado anteriormente. Esto mejora significativamente la eficiencia del componente, especialmente en situaciones donde los cálculos son más intensivos en recursos.
El hook useCallback
en React proporciona una forma de memorizar funciones. La principal ventaja es evitar la creación de nuevas funciones en cada renderización, devolviendo la función memorizada previamente si las dependencias no han cambiado.
Así es como se utiliza el hook useCallback
:
const callbackMemorizado = useCallback(callback, arrayDependencia)
El problema que aborda useCallback
se hace evidente cuando necesitamos pasar funciones a componentes hijos y queremos evitar la creación de nuevas instancias de esas funciones en cada renderización. Tomemos el ejemplo del componente Counter
:
function Counter({ count, onIncrement }: Props) {
const handleIncrement = () => {
onIncrement(count)
}
return (
<div>
<p>Contador: {count}</p>
<button onClick={handleIncrement}>Incrementar</button>
</div>
)
}
En este caso, handleIncrement
se recrea en cada renderización, lo cual no es eficiente, especialmente si onIncrement
y count
no han cambiado. useCallback
aborda esto al memorizar la función solo cuando cambian las dependencias:
import { useCallback } from 'react'
export function Counter({ count, onIncrement }: Props) {
const handleIncrement = useCallback(() => {
onIncrement(count)
}, [count, onIncrement
]) // solo recrear si count u onIncrement cambian
return (
<div>
<p>Contador: {count}</p>
<button onClick={handleIncrement}>Incrementar</button>
</div>
)
}
Con useCallback
, si count
o onIncrement
no han cambiado, se evita la creación de una nueva función y se reutiliza la función calculada previamente. Esto mejora la eficiencia y contribuye a un rendimiento del componente más óptimo.
useId
es un hook de React diseñado para generar identificadores únicos, ideales para asignarlos a atributos de etiquetas HTML. Esta práctica resulta especialmente útil para mejorar la accesibilidad al establecer relaciones específicas entre elementos.
Así es como se utiliza el hook useId
:
const passwordHintId = useId()
A continuación, el ID generado se utiliza en diferentes atributos:
<>
<input type='password' aria-describedby={passwordHintId} />
<p id={passwordHintId}>
</>
El problema que aborda useId
surge cuando necesitamos asignar identificadores únicos a elementos HTML, especialmente en situaciones en las que un componente puede aparecer múltiples veces en la pantalla. Tomemos el ejemplo del componente PasswordField
:
import { useId } from 'react'
export function PasswordField() {
const passwordHintId = useId()
return (
<>
<label>
Contraseña:
<input type='password' aria-describedby={passwordHintId} />
</label>
<p id={passwordHintId}>
La contraseña debe tener 18 caracteres y contener caracteres especiales.
</p>
</>
)
}
En este caso, useId
se utiliza para generar un ID único (passwordHintId
) que se asigna tanto al atributo aria-describedby
del input como al id
del párrafo. Esto asegura que aunque PasswordField
aparezca varias veces en la pantalla, los IDs generados no entrarán en conflicto.
export default function App() {
return (
<>
<h2>Elegir contraseña</h2>
<PasswordField />
<h2>Confirmar contraseña</h2>
<PasswordField />
</>
)
}
En el componente App
, donde se usa PasswordField
dos veces, useId
asegura que los identificadores generados automáticamente eviten duplicados y proporcionen una solución robusta para asignar IDs únicos en contextos repetidos. Esto contribuye a mejorar la accesibilidad y consistencia en la aplicación.
useReducer
es un hook de React diseñado para gestionar el estado de un componente mediante un enfoque basado en acciones y reducciones, similar al patrón utilizado en Redux. Este hook se prefiere sobre useState
cuando el estado de un componente es más complejo o cuando las actualizaciones de estado dependen del estado anterior o de acciones previas.
useReducer
toma dos argumentos: una función de reducción y el estado inicial. La función de reducción recibe dos argumentos: el estado actual y una acción que describe cómo debería cambiar el estado. La función de reducción devuelve el nuevo estado.
Así es como se utiliza el hook useReducer
:
const [state, dispatch] = useReducer(reducer, argumentoInicial, init)
El problema que aborda useReducer
surge cuando el estado de un componente se vuelve más complejo y las actualizaciones de estado dependen de múltiples factores o acciones. Tomemos el ejemplo del componente Counter
:
import { useState } from 'react'
export const Counter = () => {
const [count, setCount] = useState(0)
const handleIncrement = () => {
setCount(count + 1)
}
const handleDecrement = () => {
setCount(count - 1)
}
return (
<div>
<p>Contador: {count}</p>
<button onClick={handleIncrement}>Incrementar</button>
<button onClick={handleDecrement}>Decrementar</button>
</div>
)
}
En este ejemplo, usamos useState
para gestionar el estado del contador. Si bien esto puede funcionar para casos simples, a medida que la lógica del estado se vuelve más compleja o depende de acciones previas, el código puede volverse más difícil de mantener. useReducer
proporciona una estructura más organizada y una solución más escalable para manejar estados y acciones complejas en situaciones donde el código podría volverse confuso usando solo useState
. Abordaríamos este problema con useReducer
de la siguiente manera:
// Define la función de reducción
const counterReducer = (state: State, action: Action) => {
switch (action.type) {
case 'incrementar':
return { count: state.count + 1 }
case 'decrementar':
return { count: state.count - 1 }
default:
return state
}
}
export const Counter = () => {
// Usa useReducer para gestionar el estado del contador
const [state, dispatch] = useReducer(counterReducer, { count: 0 })
return (
<div>
<p>Contador: {state.count}</p>
<button onClick={() => dispatch({ type: 'incrementar' })}>Incrementar</button>
<button onClick={() => dispatch({ type: 'decrementar' })}>Decrementar</button>
</div>
)
}
useImperativeHandle
en React es una herramienta útil cuando necesitas controlar y personalizar la interfaz externa de un componente funcional, especialmente al trabajar con refs. Imagina el desafío al que te enfrentarías sin este gancho.
useImperativeHandle
:Supongamos que tenemos un componente funcional llamado FancyInput
que encapsula un campo de texto. Queremos que el componente padre acceda y manipule el valor del campo directamente y también active el enfoque en ese campo. Sin useImperativeHandle
, podríamos terminar con un código menos eficiente y propenso a errores.
export const FancyInput = () => {
const inputRef = useRef<HTMLInputElement>(null)
const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus()
}
}
return (
<div>
<input ref={inputRef} />
{/* Desafío: ¿Cómo exponer el método `focusInput` al componente padre? */}
</div>
)
}
En este escenario, exponer el método focusInput
al componente padre sería una tarea desafiante e inelegante. Aquí es donde entra en juego useImperativeHandle
.
useImperativeHandle
:export const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null)
// Usa useImperativeHandle para personalizar la interfaz externa
useImperativeHandle(ref, () => ({
focus: () => {
if (inputRef.current) {
inputRef.current.focus()
}
},
getValue: () => {
return inputRef.current ? inputRef.current.value : ''
},
}))
return (
<div>
<input ref={inputRef} />
</div>
)
})
Con useImperativeHandle
, ahora podemos exponer selectivamente las funciones que queremos que el componente padre utilice. Esta solución mejora la claridad del código y proporciona una interfaz externa más eficiente y controlada.
useLayoutEffect
en React es un gancho potente que te permite realizar operaciones síncronas después de que se hayan completado todas las mutaciones en el DOM, pero antes de que el navegador repinte la pantalla. Imagina el desafío al que te enfrentarías sin este gancho.
useLayoutEffect
:Imagina un escenario en el que queremos medir las dimensiones de un elemento DOM y, basándonos en esas dimensiones, realizar ciertas acciones en nuestro componente funcional ResizableBox
. Sin useLayoutEffect
, podríamos tener problemas al realizar estas mediciones de manera asíncrona.
export const ResizableBox = () => {
const boxRef = useRef<HTMLDivElement>(null)
const [boxWidth, setBoxWidth] = useState(0)
useEffect(() => {
// Desafío: ¿Cómo medir las dimensiones de boxRef de manera síncrona?
// Las mediciones pueden no estar disponibles de inmediato.
// Esto puede causar problemas si dependemos de las dimensiones para realizar acciones.
}, [])
return <div ref={boxRef}>{/* Contenido de la caja redimensionable */}</div>
}
En este escenario, realizar mediciones síncronas de las dimensiones de boxRef
es desafiante con useEffect
porque las mediciones pueden no estar disponibles de inmediato. Aquí es donde entra en juego useLayoutEffect
.
useLayoutEffect
:export const ResizableBox = () => {
const boxRef = useRef<HTMLDivElement>(null)
const [boxWidth, setBoxWidth] = useState(0)
useLayoutEffect(() => {
// Usa useLayoutEffect para medir las dimensiones de boxRef de manera síncrona
if (boxRef.current) {
setBoxWidth(boxRef.current.offsetWidth)
}
}, [])
return <div ref={boxRef}>{/* Contenido de la caja redimensionable */}</div>
}
Con useLayoutEffect
, podemos realizar mediciones síncronas después de que el navegador haya completado todas las mutaciones en el DOM pero antes de repintar la pantalla. Esto asegura que las mediciones estén disponibles de inmediato y evita problemas potenciales relacionados con la asincronía.
useDebugValue
en React es un gancho especializado diseñado para mejorar la experiencia de depuración al proporcionar información adicional sobre el estado de un componente. Su principal utilidad es facilitar la identificación de componentes en las herramientas de desarrollo del navegador.
La firma básica para useDebugValue
es la siguiente:
useDebugValue(value)
Al depurar una aplicación de React, es común enfrentarse al desafío de identificar rápidamente el componente de interés dentro de las herramientas de desarrollo del navegador. Sin useDebugValue
, la información sobre un componente puede ser limitada y no específica.
Así es cómo se usa el gancho useDebugValue
:
import { useDebugValue } from 'react'
const useCustomHook = () => {
const state =
/* lógica del gancho */
// Proporcionar un nombre descriptivo para facilitar la identificación en las herramientas de desarrollo
useDebugValue('Custom Hook')
return state
}
import { useDebugValue } from 'react'
const useArrayHook = (array: string[]) => {
// lógica del gancho
// Mostrar el contenido del array en las herramientas de desarrollo
useDebugValue(array.join(', '))
return /* resultado */
}
import { useState, useDebugValue } from 'react'
const useCounter = () => {
const [count, setCount] = useState(0)
// Utilizar el valor del contador como información en las herramientas de desarrollo
useDebugValue(`Contador: ${count}`)
return [count, setCount]
}
Al usar useDebugValue
, puedes personalizar la información que se muestra sobre tu gancho o componente en las herramientas de desarrollo, facilitando su identificación y depuración durante el desarrollo.
En esta exploración de React Hooks, desde los conceptos más básicos hasta los más avanzados, confío en que hayas descubierto el fascinante mundo que estos ganchos ofrecen para simplificar y potenciar tus desarrollos en React. ¡Feliz codificación! ✨