a
Más sobre los hooks de React
Los ejercicios de esta parte del curso difieren un poco de los anteriores. Como de costumbre, hay algunos ejercicios relacionados con la teoría de este capítulo. Los demás capítulos de esta parte no tienen ejercicios separados.
Además, esta parte contiene una serie más amplia de ejercicios que amplía la aplicación BlogList creada en las partes 4 y 5. Puedes encontrar esos ejercicios aquí.
Hooks de React
React ofrece 18 hooks incorporados diferentes, de los cuales los más populares son useState y useEffect, que ya hemos utilizado extensivamente.
En la parte 5 usamos useRef y useImperativeHandle, que permitieron que un componente proporcionara acceso a sus funciones a otros componentes. En la parte 6 utilizamos useContext para implementar un estado global.
En los últimos años, los hooks se han convertido en la forma estándar en que las librerías exponen sus APIs. A lo largo del curso ya hemos visto varios ejemplos: Zustand proporciona useStore para acceder al estado global, React Router expone useNavigate y useParams para la navegación programática y el acceso a parámetros de la URL, y React Query ofrece useQuery y useMutation para gestionar el estado del servidor.
Como se mencionó en la parte 1, los hooks no son funciones normales y cuando los usamos tenemos que cumplir con ciertas reglas o limitaciones. Recapitulemos las reglas del uso de hooks, copiadas literalmente de la documentación oficial de React:
Evita utilizar Hooks dentro de loops, condicionales o funciones anidadas. En su lugar, utiliza los Hooks únicamente en el nivel superior de tu función de React.
Los Hooks sólo deben ser utilizados durante la renderización de un componente de función en React:
- Utilízalos en el nivel superior del cuerpo de un componente de función.
- Utilízalos en el nivel superior del cuerpo de un Hook personalizado.
Existe un plugin de ESLint que se puede usar para verificar que la aplicación utiliza los hooks correctamente:

Además de los hooks que ya hemos utilizado, React proporciona varios hooks incorporados que merece la pena conocer. En esta sección veremos dos de ellos, useMemo y useCallback, ambos relacionados con la optimización del rendimiento. Después pasaremos a los hooks personalizados, que permiten empaquetar cualquier combinación de hooks en una función reutilizable propia.
useMemo
Cada vez que un componente de React se vuelve a renderizar, se ejecuta de nuevo todo el cuerpo de la función. Para la mayoría de los componentes esto no supone ningún problema, pero en ocasiones un componente realiza un cálculo costoso —como filtrar una lista grande, ordenar datos u obtener un valor complejo— y repetirlo en cada renderizado desperdicia tiempo.
useMemo permite almacenar en caché el resultado de un cálculo entre renderizados. Recibe una función que realiza el cálculo y un array de dependencias. React solo vuelve a ejecutar la función cuando cambia alguna de las dependencias; en caso contrario, devuelve el resultado almacenado anteriormente.
Consideremos un componente que renderiza una larga lista de elementos filtrados por un término de búsqueda:
import { useState } from 'react'
const expensiveCalculation = () => {
let sum = 0
for (let i = 0; i < 100000; i++) sum += i
return sum
}
const ITEMS = Array.from({ length: 10000 }, (_, i) => `item ${i + 1}`)
const FilteredList = () => {
const [filter, setFilter] = useState('')
const [darkMode, setDarkMode] = useState(false)
console.log('filtering...')
const filtered = ITEMS.filter(item => {
expensiveCalculation()
return item.includes(filter)
})
return (
<div style={{ background: darkMode ? '#333' : '#fff' }}>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="filter items"
/>
<button onClick={() => setDarkMode(!darkMode)}>toggle dark mode</button>
<ul>
{filtered.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
)
}
export default FilteredListFiltrar la lista lleva ahora tiempo, en parte gracias a la ralentización artificial que hemos introducido.
El problema del componente es que al hacer clic en el botón del modo oscuro se vuelven a filtrar los 10 000 elementos aunque el texto del filtro no haya cambiado.
Podemos solucionarlo con useMemo:
import { useState, useMemo } from 'react'
const FilteredList = () => {
const [filter, setFilter] = useState('')
const [darkMode, setDarkMode] = useState(false)
const filtered = useMemo(() => { console.log('filtering...')
return ITEMS.filter(item => {
expensiveCalculation()
return item.includes(filter)
})
}, [filter])
return (
<div style={{ background: darkMode ? '#333' : '#fff' }}>
//...
</div>
)
}Con useMemo, el filtrado costoso solo se ejecuta cuando cambia filter. Alternar el modo oscuro únicamente actualiza el color de fondo y la lista filtrada almacenada en caché se devuelve de inmediato.
El array de dependencias funciona exactamente igual que el de useEffect: React compara cada valor con el del renderizado anterior. Si todos los valores son idénticos, se reutiliza el valor memorizado. Si alguno difiere, la función se ejecuta de nuevo y el resultado se almacena para el siguiente renderizado.
useMemo también puede utilizarse para memorizar objetos y arrays que se pasan como props, evitando renderizados innecesarios de componentes hijos que utilizan igualdad por referencia. Por ejemplo:
const App = () => {
const [filter, setFilter] = useState('')
// Without useMemo, 'options' is a new object on every render even if filter hasn't changed
const options = useMemo(() => ({ caseSensitive: false, filter }), [filter])
return <SearchResults options={options} />
}useMemo es una optimización del rendimiento; no deberías utilizarlo por defecto. La memorización prematura añade complejidad sin aportar beneficios cuando el cálculo es rápido. Mide primero y añade useMemo solo cuando hayas confirmado que un cálculo concreto constituye un cuello de botella.
React.memo
Mientras que useMemo almacena en caché el resultado de un cálculo dentro de un componente, React.memo adopta un enfoque diferente: almacena la salida renderizada de un componente completo. React.memo no es un hook, sino un componente de orden superior, y lo tratamos aquí porque complementa bien a useMemo. Cuando un componente está envuelto en React.memo, React omite su nuevo renderizado si sus props no han cambiado desde el renderizado anterior.
const MyComponent = React.memo(({ value }) => {
console.log('rendered')
return <div>{value}</div>
})Sin React.memo, MyComponent se vuelve a renderizar cada vez que lo hace su componente padre, aunque value sea el mismo. Con él, React compara las props anteriores y nuevas mediante una igualdad superficial y solo vuelve a renderizar cuando algo ha cambiado realmente.
Ten en cuenta que React.memo solo comprueba las props. Si el componente utiliza un valor del contexto o su propio estado, seguirá renderizándose de nuevo cuando estos cambien.
React.memo se combina de forma natural con useMemo: useMemo evita que se repitan cálculos costosos, mientras que React.memo impide que el propio componente vuelva a renderizarse.
Si un componente memorizado recibe una nueva referencia a una función u objeto en cada renderizado, la memorización queda anulada. Aquí es donde entra en juego useCallback.
useCallback
Las funciones definidas dentro de un componente se vuelven a crear como objetos nuevos en cada renderizado. Normalmente esto es inofensivo, pero se convierte en un problema en dos situaciones concretas:
- Un componente hijo envuelto en React.memo recibe la función como prop. Como la función es un objeto nuevo cada vez, el hijo siempre detecta una prop distinta y se vuelve a renderizar, anulando el propósito de la memorización.
- Una función aparece como dependencia de useEffect o useMemo. Si se crea una función nueva en cada renderizado, el efecto o el valor memorizado se vuelven a ejecutar en cada renderizado.
useCallback resuelve este problema almacenando en caché la propia función entre renderizados y devolviendo el mismo objeto función mientras no cambien sus dependencias. Recibe una función y un array de dependencias, con una estructura idéntica a la de useMemo.
Veamos un ejemplo concreto. Tenemos un componente NoteList costoso de renderizar, así que lo envolvemos en React.memo:
// React.memo makes this component skip re-rendering if its props haven't changed
const NoteList = memo(({ onDelete, notes }) => {
console.log('NoteList rendered')
return (
<ul>
{notes.map(note => (
<li key={note.id}>
{note.content}
<button onClick={() => onDelete(note.id)}>delete</button>
</li>
))}
</ul>
)
})
const App = () => {
const [notes, setNotes] = useState([
{ id: 1, content: 'Learn React' },
{ id: 2, content: 'Learn hooks' },
{ id: 3, content: 'Learn useMemo' },
{ id: 4, content: 'Learn useCallback' },
{ id: 5, content: 'Build something cool' },
])
const [newNote, setNewNote] = useState('')
const handleDelete = (id) => {
setNotes(notes => notes.filter(note => note.id !== id))
}
const handleAdd = () => {
setNotes(notes => [...notes, { id: Date.now(), content: newNote }])
setNewNote('')
}
return (
<div>
<input value={newNote} onChange={e => setNewNote(e.target.value)} />
<button onClick={handleAdd}>add</button>
<NoteList notes={notes} onDelete={handleDelete} />
</div>
)
}El problema es que handleDelete está definida como una función normal dentro de App. Cada vez que App se vuelve a renderizar —lo que ocurre con cada pulsación en el campo de la nueva nota— se crea un objeto función completamente nuevo y se pasa a NoteList como prop onDelete.
Desde la perspectiva de React.memo, la prop ha cambiado, por lo que NoteList vuelve a renderizarse aunque la lista no haya cambiado:

Podemos solucionarlo con useCallback, que devuelve el mismo objeto función entre renderizados mientras sus dependencias no cambien:
import { useState, useCallback, memo } from 'react'
const App = () => {
const [notes, setNotes] = useState([])
const [newNote, setNewNote] = useState('')
const handleDelete = useCallback((id) => { setNotes(notes => notes.filter(note => note.id !== id)) }, []) // no external dependencies: this function never needs to change
// ...
return (
// ...
)
}Ahora handleDelete es estable: React devuelve exactamente el mismo objeto función en cada renderizado, por lo que React.memo no detecta ningún cambio en la prop onDelete y omite por completo el nuevo renderizado de NoteList.
Al igual que useMemo, utiliza useCallback solo cuando tengas un problema concreto, como un componente hijo memorizado que se renderiza innecesariamente o un useEffect que se ejecuta con demasiada frecuencia debido a una dependencia de función. Añadirlo en todas partes hace que el código sea más difícil de leer sin aportar ninguna mejora de rendimiento.
Hooks personalizados
React ofrece la opción de crear nuestros propios hooks personalizados. Según React, el propósito principal de los hooks personalizados es facilitar la reutilización de la lógica utilizada en los componentes.
Crear tus propios Hooks te permite extraer la lógica de los componentes en funciones reutilizables.
Los hooks personalizados son funciones normales de JavaScript que pueden utilizar cualquier otro hook, siempre que respeten las reglas de los hooks. Además, el nombre de los hooks personalizados debe comenzar con la palabra use.
La idea clave es que cualquier lógica con estado que te encuentres duplicando entre componentes es candidata a extraerse a un hook personalizado. Cada llamada al mismo hook crea una porción de estado independiente. Esto es lo que distingue un hook personalizado de una función de utilidad corriente.
Ya hemos implementado varios hooks personalizados en la parte 6. Los hooks useNotes y useNoteActions se crearon en la sección sobre Zustand, y useCounter se definió en la sección sobre React Query y Context.
Hook de contador
Implementamos una aplicación de contador en la parte 1, que puede tener su valor incrementado, reducido o reiniciado. El código de la aplicación es el siguiente:
import { useState } from 'react'
const App = () => {
const [counter, setCounter] = useState(0)
return (
<div>
<div>{counter}</div>
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
<button onClick={() => setCounter(counter - 1)}>
minus
</button>
<button onClick={() => setCounter(0)}>
zero
</button>
</div>
)
}Extraigamos la lógica del contador en su propio hook personalizado. El código del hook es el siguiente:
const useCounter = () => {
const [value, setValue] = useState(0)
const increase = () => {
setValue(value + 1)
}
const decrease = () => {
setValue(value - 1)
}
const zero = () => {
setValue(0)
}
return {
value,
increase,
decrease,
zero
}
}Nuestro hook personalizado utiliza el hook useState internamente para crear su propio estado. El hook devuelve un objeto, cuyas propiedades incluyen el valor del contador, así como funciones para manipular el valor.
Los componentes de React pueden utilizar el hook como se muestra a continuación:
const App = () => {
const counter = useCounter()
return (
<div>
<div>{counter.value}</div>
<button onClick={counter.increase}>
plus
</button>
<button onClick={counter.decrease}>
minus
</button>
<button onClick={counter.zero}>
zero
</button>
</div>
)
}Al hacer esto, podemos extraer el estado del componente App y su manipulación por completo en el hook useCounter. La gestión del estado y la lógica del contador ahora es responsabilidad del hook personalizado.
El mismo hook podría reutilizarse en la aplicación que realizaba un seguimiento de la cantidad de clics realizados en los botones izquierdo y derecho:
const App = () => {
const left = useCounter()
const right = useCounter()
return (
<div>
{left.value}
<button onClick={left.increase}>
left
</button>
<button onClick={right.increase}>
right
</button>
{right.value}
</div>
)
}La aplicación crea dos contadores completamente separados. El primero se asigna a la variable left y el otro a la variable right. Cada llamada a useCounter crea su propia porción de estado independiente.
Hooks personalizados y nuevos renderizados de componentes
Una pregunta natural en este punto es: ¿cuándo se vuelve a renderizar realmente un componente que utiliza un hook personalizado?
La respuesta es sencilla cuando se entiende qué es en realidad un hook personalizado. Desde la perspectiva del componente, un hook personalizado no es una entidad separada. Es simplemente una parte de la lógica del propio componente que se ha trasladado a una función independiente. Esto significa que todo el estado y los efectos definidos dentro del hook pertenecen al componente que lo llama, no al propio hook.
En consecuencia, las reglas de renderizado son exactamente las mismas que con los hooks incorporados. El componente vuelve a renderizarse cuando cambia un estado gestionado dentro del hook, cuando cambia un valor de contexto al que el hook está suscrito o cuando cualquier hook al que el hook personalizado llama internamente provoca un nuevo renderizado.
En cambio, acciones como reasignar variables normales dentro del hook o que cambien por sí solos los argumentos pasados al hook no provocan un nuevo renderizado.
Los argumentos merecen un análisis más detallado. Pasar un nuevo valor a un hook no programa por sí mismo un nuevo renderizado, pero si el hook utiliza ese argumento como dependencia de un useEffect o un useMemo, el cambio hará que el efecto o el valor memorizado vuelvan a ejecutarse. Si esto, a su vez, llama a una función actualizadora del estado, el componente se renderizará de nuevo.
Una forma útil de entenderlo es imaginar que copias y pegas todo el código del hook personalizado directamente dentro del componente. El comportamiento de renderizado sería idéntico. El hook solo sirve para organizar ese código; no es un límite que React trate de una manera especial.
const useCounter = () => {
const [count, setCount] = useState(0) // this state belongs to the calling component
return { count, increment: () => setCount(c => c + 1) }
}
const MyComponent = () => {
const { count, increment } = useCounter()
// re-renders whenever the count state inside the hook is updated
}Hook para campos de formulario
Tratar con formularios en React es algo complicado. La siguiente aplicación presenta al usuario un formulario que le solicita que ingrese su nombre, fecha de nacimiento y altura:
const App = () => {
const [name, setName] = useState('')
const [born, setBorn] = useState('')
const [height, setHeight] = useState('')
return (
<div>
<form>
name:
<input
type='text'
value={name}
onChange={(event) => setName(event.target.value)}
/>
<br/>
birthdate:
<input
type='date'
value={born}
onChange={(event) => setBorn(event.target.value)}
/>
<br />
height:
<input
type='number'
value={height}
onChange={(event) => setHeight(event.target.value)}
/>
</form>
<div>
{name} {born} {height}
</div>
</div>
)
}Cada campo del formulario tiene su propio estado. Para mantener el estado del formulario sincronizado con los datos proporcionados por el usuario, tenemos que registrar un controlador onChange apropiado para cada uno de los elementos input. El patrón es idéntico para todos los campos; solo cambia el nombre de la variable de estado. Este es exactamente el tipo de repetición que los hooks personalizados están diseñados para eliminar.
Definamos nuestro propio hook personalizado useField, que simplifica la gestión del estado del formulario:
const useField = (type) => {
const [value, setValue] = useState('')
const onChange = (event) => {
setValue(event.target.value)
}
return {
type,
value,
onChange
}
}La función de hook recibe el tipo de campo de entrada como parámetro. Devuelve todos los atributos requeridos por el input: su tipo, valor y el controlador onChange.
El hook se puede utilizar de la siguiente manera:
const App = () => {
const name = useField('text')
// ...
return (
<div>
<form>
<input
type={name.type}
value={name.value}
onChange={name.onChange}
/>
// ...
</form>
// ...
<div>
{name.value} {born} {height} </div>
</div>
)
}Propagación de atributos con spread
Podríamos simplificar un poco más las cosas. Dado que el objeto name tiene exactamente todos los atributos que el elemento input espera recibir como props, podemos pasar los props al elemento usando la sintaxis spread(spread syntax) de la siguiente manera:
<input {...name} /> Como indica el ejemplo en la documentación de React, las siguientes dos formas de pasar props a un componente logran exactamente el mismo resultado:
<Greeting firstName='Arto' lastName='Hellas' />
const person = {
firstName: 'Arto',
lastName: 'Hellas'
}
<Greeting {...person} />La aplicación se simplifica en el siguiente formato:
const App = () => {
const name = useField('text')
const born = useField('date')
const height = useField('number')
return (
<div>
<form>
name:
<input {...name} />
<br/>
birthdate:
<input {...born} />
<br />
height:
<input {...height} />
</form>
<div>
{name.value} {born.value} {height.value}
</div>
</div>
)
}Tratar con formularios se simplifica enormemente cuando los desagradables detalles esenciales relacionados con la sincronización del estado del formulario se encapsulan dentro de nuestro hook personalizado.
Persistencia del estado con un hook personalizado
Los hooks personalizados pueden combinar varios hooks incorporados para encapsular comportamientos más complejos. Una funcionalidad que se necesita habitualmente es persistir el estado en localStorage para que sobreviva a una recarga de la página. El siguiente hook useLocalStorage envuelve useState y mantiene el valor sincronizado con localStorage:
import { useState } from 'react'
const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
return initialValue
}
})
const setValue = (value) => {
try {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue]
}El hook recibe una clave de almacenamiento y un valor inicial. En el primer renderizado lee el valor de localStorage y recurre a initialValue si todavía no hay nada almacenado. La función actualizadora que devuelve modifica al mismo tiempo tanto el estado de React como localStorage.
Un componente que lo utiliza tiene exactamente el mismo aspecto que uno que emplea useState directamente:
const App = () => {
const [name, setName] = useLocalStorage('name', '')
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<p>Hello, {name}! (your name is stored in localStorage)</p>
</div>
)
}El componente no sabe que localStorage está implicado. Esa responsabilidad queda completamente oculta dentro del hook.
Más sobre hooks
Los hooks personalizados no son solo una herramienta para reutilizar código; también proporcionan una forma mejor de dividirlo en partes modulares más pequeñas.
Internet está comenzando a llenarse con más y más material útil relacionado con los hooks. Vale la pena consultar las siguientes fuentes:

