useEvent Hook
Como ya saben, antes de ir a la solución, primero vamos a entender el problema. En este artículo vas a encontrar información sobre:
Imaginemos que queremos implementar un input para filtrar resultados de búsqueda. Normalmente podríamos hacer algo así:
import React from 'react'
import { SendButton } from './Buttons/SendButton'
function App() {
const [search, setSearch] = React.useState('')
const onSubmit = () => {
console.log('input value:', text)
// sendToApi(text)
})
return (
<>
<input type="text" onChange={(e) => setText(e.target.value)} />
<SendButton onSubmit={onSubmit}/>
</>
)
}
export default App
Mientras que en SendButton tenemos una implementación muy sencilla:
// Typescript Code
import * as React from 'react'
interface SendButtonProps {
onSubmit: () => void
}
const Component: React.FC<SendButtonProps> = ({ onSubmit }) => {
return (
<button onClick={onSubmit}>
Buscar
</button>
)
}
export const SendButton = Component
Y quedaría algo hermoso como esto:
Como vemos en nuestro Profiler (gracias a nuestras React Developer Tools) cada vez que escribimos algo sobre nuestro Input, nuestro botón se va a re-renderizar.
Y también veremos que vamos a tener un paso dentro de nuestro profiler por cada re-renderizado que se realice en el componente padre:
Algo que veo en muchas implementaciones es que se tiende a abusar de la nobleza de la función memo y se hace algo como esto:
// Typescript Code
import * as React from 'react'
interface SendButtonProps {
onSubmit: () => void
}
const Component: React.FC<SendButtonProps> = ({ onSubmit }) => {
return (
<button onClick={onSubmit}>
Buscar
</button>
)
}
export const SendButton = React.memo(Component)
Recordemos que memo va a lograr una memoización efectiva sobre nuestro componente siempre y cuando las props que lleguen a este componente no cambien.
Con esto pretendo lograr que lo usemos más a conciencia, es decir, en esta implementación, podemos ver que como el estado de App cambia (gracias al setSearch), nuestra función onSubmit se volverá a crear con cada cambio de estado por lo que va a tener una nueva instancia en cada re-renderizado y, por lo tanto, a nuestro componente de SendButton siempre le llegará una función nueva.
Algo que se podría hacer para evitar esto sería wrappear nuestra función de onSubmit con el hook de useCallback y es algo que se tiende a hacer pero una vez hecho esto, por lo general no verificamos el comportamiento de nuestro componente, simplemente aceptamos que esto va a funcionar y listo.
Recomendado por LinkedIn
const onClick = React.useCallback(() => {
console.info('input value: ', text)
// const response = sendToApi(text)
}, [text])
En este caso particular, no tiene mucho sentido wrappear nuestro manejador con un useCallback ya que tiene una dependencia del state (text) por lo que cuando cambie nuestro estado, esta función se volverá a crear y cuando se lo pasemos como prop la referencia cambiará y el componente se volverá a re-renderizar.
useEvent al rescate
Lo que haremos con este custom hook es crear una referencia estable a nuestro evento (que será pasado como callback a nuestra implementación)
Lo que devolverá useEvent será una función memoizada que vivirá en una ref (inmutable a los cambios de estado)
useEvent.ts
import React from 'react'
type AnyFunction = (...args: unknown[]) => unknown
export function useEvent<T extends AnyFunction> (callback: T): T {
const ref = React.useRef<AnyFunction | undefined>(undefined)
React.useEffect(() => {
ref.current = callback
}, [callback])
return React.useCallback<AnyFunction>((...args: AnyFunction) => ref.current(...args), [])
}
useEvent.test.ts (con vitest)
import { act, renderHook } from '@testing-library/react'
import { useEvent } from '../useEvent'
describe('useEvent', () => {
it('should execute the callback', () => {
const fn = vi.fn()
const { result } = renderHook(() => useEvent(fn))
act(() => {
result.current()
})
expect(fn).toHaveBeenCalledOnce()
})
it('should memoize the handler', () => {
const fn = vi.fn()
const { result, rerender } = renderHook(() => useEvent(fn))
const eventHandlerOne = result.current
rerender()
const eventHandlerTwo = result.current
expect(eventHandlerOne).toStrictEqual(eventHandlerTwo)
})
})
Si bien los re-renders se mantienen, el Componente no se vuelve a crear en cada cambio de estado. Podemos chequearlo en el Profiler
En el RFC de useEvent se propone una implementación con useLayoutEffect (que en mi opinión es lo ideal porque termina siendo más general para esta implementación)
Finalmente, para utilizar este hook como manejar de evento podríamos hacer algo como esto:
...
const onClick = useEvent(() => {
console.info('input value: ', text)
// const response = sendToApi(text)
})
...
...
...
<input type="text" onChange={(e) => { setText(e.target.value) }} />
<SendButton onClick={onClick} />
Nota: Este hook debe utilizarse para memoizar referencia a manejadores de eventos. Un mal caso de uso para este hook sería guardar valores:
const getListOfData = useEvent(() => {
// some boring operations
return [1, 2, 4];
});
Desarrollador Nodejs| Java |Kotlin | JS |React-Nextjs | React Native |Typescript |Docker | MongoDB | MySQL| Tailwindcss
6 mesesAvísame cuando se junten y hagan un tipo webinar o algo así como una compensación de programación jaja
Desarrollador Nodejs| Java |Kotlin | JS |React-Nextjs | React Native |Typescript |Docker | MongoDB | MySQL| Tailwindcss
6 mesesEl hook garfio 🪝 jajaja
Frontend Developer | React Developer
6 mesesMuy bueno Fran ! Siempre dando la data importante !
Software Developer
6 mesesMuy bueno Fran! 🙌🏾
Tech Lead en Desky | Ayudo a impulsar proyectos con software escalable y mantenible utilizando las tecnologías React | React Native | Next.Js | TypeScript | JavaScript | Conexión con AI
6 mesesExcelente articulo fran, me parece que la forma de explicar la diferencia entre useEffect y useLayoutEffect es muy buena, más claro imposible 💪