useEvent Hook

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:

  • Performance
  • useEffect vs useLayoutEffect
  • Memorización de componentes y de funciones con memo y useCallback

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:

Los estilos son lo mio, ya lo saben 😎


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.

Profiler de React Developer Tools


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:

En este caso, veremos que hubo 4 interacciones (escribí 'hola')


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.

  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)


Recordemos que useEffect es asíncrono y se ejecuta después del repintado mientras que useLayoutEffect es bloqueante y ocurre previo al repintado


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];
  });        
Ariel Zarate

Desarrollador Nodejs| Java |Kotlin | JS |React-Nextjs | React Native |Typescript |Docker | MongoDB | MySQL| Tailwindcss

6 meses

Avísame cuando se junten y hagan un tipo webinar o algo así como una compensación de programación jaja

Ariel Zarate

Desarrollador Nodejs| Java |Kotlin | JS |React-Nextjs | React Native |Typescript |Docker | MongoDB | MySQL| Tailwindcss

6 meses

El hook garfio 🪝 jajaja

Nicolas Bomben

Frontend Developer | React Developer

6 meses

Muy bueno Fran ! Siempre dando la data importante !

Muy bueno Fran! 🙌🏾

Isaias Mella

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 meses

Excelente articulo fran, me parece que la forma de explicar la diferencia entre useEffect y useLayoutEffect es muy buena, más claro imposible 💪

Inicia sesión para ver o añadir un comentario.

Más artículos de Franco Di Leo

  • El asombroso mundo de las curvas de nivel y Python

    El asombroso mundo de las curvas de nivel y Python

    Una curva de nivel es un concepto que resulta de la intersección de una superficie: Z = f(x,y) con un plano Z = k donde…

  • P-Value

    P-Value

    Background previo Para entender el concepto de p-value en el ámbito de estadística general, tenemos que entender…

    2 comentarios
  • A/B Testing

    A/B Testing

    Salimos a producción con un feature que creemos que la va a romper, el Feature X nos va a ayudar a convertir mejor y va…

  • Compound Components Pattern en React

    Compound Components Pattern en React

    Antes de llegar a la solución, me gustaría navegar sobre el problema. Imaginemos que tenemos que compartir un estado o…

    2 comentarios
  • Auditoria de Identidad

    Auditoria de Identidad

    La primera vez que me topé con este concepto, me quedé sorprendido. A veces creemos que para destacar en nuestro sector…

    1 comentario
  • La cámara de Eco

    La cámara de Eco

    Okey, arranquemos con el problema y dando un poco de contexto. Estas últimas 3 semanas pasé mucho tiempo con el celular…

    1 comentario
  • Outliers y diagramas de caja

    Outliers y diagramas de caja

    ¿Qué son los outliers? Spoiler alert: Podes ver este concepto en mi video de Youtube https://www.youtube.

    1 comentario

Otros usuarios han visto

Ver temas