Custom Hook en React: Creando Lógica Reutilizable Paso a Paso 2025

Custom Hook en React: Creando Lógica Reutilizable Paso a Paso 2025

juan correa

Juan Correa

Desarrollador de Software Senior

En este post, profundizaremos en el patrón de diseño Custom Hook en React JS. Aprende cómo este patrón puede transformar tu manera de escribir y estructurar aplicaciones en React.

Table of Contents

¿Qué es un Custom Hook?

A partir de la versión 16.8 de React se publicaron oficialmente los React hooks. De acuerdo a la documentación oficial de React:

“Un Hook personalizado es una función de JavaScript cuyo nombre comienza con ”use” y que puede llamar a otros Hooks”.

Los custom hooks consisten en la posibilidad de crear nuestros propios hooks para poder reusar lógica de estado entre componentes, esto de una manera que antes no se podía hacer con los componentes solamente.

Al final de este capítulo vas a poder ser capaz de crear tus propios hooks e incluso aplicar el custom hook que haremos en el ejemplo.

¿Quieres dominar los Patrones de Diseño en React.js?

En mi ebook "Patrones avanzados en React.js", encontrarás más información sobre los patrones explicados en este post y contenido de alta calidad que te ayudará a llevar tus habilidades al siguiente nivel.

Descargar ebook gratis

Ventajas y desventajas de los Custom Hooks

Las principales ventajas de usar custom hooks son:

  • Favorece el mantenimiento al tener lógica de estado separada en funciones que tengan solo una sola responsabilidad.
  • Fácil de testear al ser una función de javascript.
  • Complemento de otros patrones: los hace más fáciles de realizar. Las principales desventajas de los custom hooks son:
  • Limitados a la versión 16.8 de React. Aunque en teoría siempre hay que actualizar las versiones de las dependencias en nuestros proyectos.

En qué casos aplicar los Custom Hooks

Para saber si necesitas crear un custom hook considera lo siguiente:

  • Tienes componentes que repiten la lógica de cómo actualizan su estado. Ejemplos:
  • Formularios.
  • Suscripciones.
  • Temporizadores.
  • Animaciones.
  • Refactorizar un componente de tipo clase que maneje un estado interno muy complejo y que sea difícil de mantener. Podrías crear un hook para simplificar la lógica de estado quedando tu componente más limpio.

Esta lista son sólo algunos ejemplos. Ten en mente que siempre que haya lógica de estado que se pueda abstraer, es posible hacerlo con un hook.

Ejemplo de un Custom Hook

Ejemplo rápido de un custom hook es:

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.log(error)
      return initialValue
    }
  })

  const setValue = (value) => {
    try {
      setStoredValue(value)
      window.localStorage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.log(error)
    }
  }

  return [storedValue, setValue]
}

Este Custom Hook te permite interactuar fácilmente con el LocalStorage del navegador, encapsulando la lógica necesaria para almacenar y recuperar valores.

Ejemplo en vídeo

Aquí tienes un video del curso completo en YouTube en caso de que prefieras verlo en este formato:

Si prefieres leer, sigue leyendo para profundizar en el tema.

Creando un Custom Hook paso a paso

Vamos a entrar más en materia haciendo una mini app que consuma apis de terceros y liste la información.

Primero vamos a implementar el código con hooks normales de React y después vamos a ir refactorizando paso a paso hasta abstraer toda la lógica de consumo de una api.

De este modo seremos capaces consumir cualquier api en cualquier componente sin reescribir la lógica necesaria.

Consumiremos la API de github y lo que consultaremos son los repositorios de Developero-Oficial, la organización en la que publico código fuente de mis tutoriales y cursos, muchos de ellos hechos en React y Nodejs.

Entonces nuestro código va a lucir de este modo en un inicio.

const Developero = () => {
  const [data, setData] = useState(null)
  useEffect(() => {
    const url = `${apiBaseUrl}/orgs/Developero-oficial/repos?sort="created"`
    fetch(url)
      .then((response) => response.json())
      .then((data) => setData(data))
  }, [])
  if (!data) {
    return null
    37
  }
  return data.map(({ name, html_url, description }) => (
    <>
      <p>
        {' '}
        <a href={html_url} target="_blank" rel="noopener noreferrer">
          {name}{' '}
        </a>
      </p>
      <p>{description}</p>
    </>
  ))
}

Puedes consultar el código fuente en este enlace: https://codesandbox.io/s/fetch-example-gigt8?file=/src/App.js:122-700. El componente Developero tiene la lógica para consultar la api de Github y enlistar los resultados. Para ello usamos los hooks useState para tener variables de estado relativas al response de la api y el useEffect para que cuando se monte el componente, consuma la api y asigne los valores correspondientes.

Nota que en el useEffect pasamos un array vacío como segundo parámetro. En este array se colocan las dependencias que necesita el hook pero en este caso, no tiene ninguna.

El resultado es que este efecto solo se ejecuta una sola vez cuando se monta el componente, haciendo lo que haría el método del ciclo de vida componentDidMount en un componente tipo clase.

Una vez explicado lo anterior, vamos a hacer el proceso de abstracción de la lógica para consumir la api.

  • El estado: Nos interesa reusar la lógica de la variable de estado. En este caso es data, que es la variable que creamos con el hook useState y usamos para asignar el response de la api.
  • La actualización de la variable de estado data es realizada en el hook useEffect donde usamos fetch y resolvemos las promesas de manera tradicional (al final lo pondremos con async await pero quiero hacerlo de manera progresiva). Una vez dicho lo anterior, pasamos a crear la primera versión de nuestro hook con la funcionalidad listada anteriormente.
const useFetch = (url) => {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetch(url, options)
      .then((response) => response.json())
      .then((data) => setData(data))
  }, [url])
  return { data }
}

Observa que nuestro custom hook no es más que una función de Javascript que hemos llamado “useFetch”. Recuerda que en un custom hook podemos usar a otros hooks, en este caso, useState y useEffect.

Recibe un parámetro que nosotros llamamos url, y lo colocamos en el array de dependencias en el useEffect. Esto significa que si el valor de url cambia, se ejecutará de nuevo el fetch.

Si nunca cambia, entonces no volverá a ejecutarse. La manera de usar useFetch se muestra a continuación.

const Developero = () => {
  const url = `${apiBaseUrl}/orgs/Developero-oficial/repos?sort=created`
  const { data } = useFetch(url)
  if (!data) {
    return null
  }
  return data.map(({ name, html_url, description }) => (
    <div key={html_url}>
      <p>
        39
        <a
          href={html_url}
          target="_blank"
          rel="noopener
noreferrer"
        >
          {name}{' '}
        </a>
      </p>
      <p>{description}</p>
    </div>
  ))
}

Con esto ya estamos abstrayendo el consumo de una api en nuestro componente (por eso pasamos la url como parámetro), delegando esa lógica en el custom hook recién creado.

Solo para complementar este ejemplo, vamos a considerar los escenarios en el que la api nos de un error y una variable que sirva como bandera para saber si el fetch está en ejecución.

const useFetch = (url) => {
  const [data, setData] = useState(null)
  const [isFetching, setIsFetching] = useState(false)
  const [error, setError] = useState(null)
  useEffect(() => {
    setIsFetching(true)
    fetch(url, options)
      .then((response) => response.json())
      .then((data) => setData(data))
      .catch((e) => setError(e))
      .finally(() => setIsFetching(false))
  }, [url])
  return { data, error, isFetching }
}

Hemos agregado dos variables de estado nuevas: isFetching para indicar cuando el fetch está en proceso y error para asignar la causa del error al hacer el fetch. Ambos los retornamos junto a data.

Para implementarlo, hacemos lo siguiente.

const Developero = () => {
  const url = `${apiBaseUrl}/orgs/Developero-oficial/repos?sort=created`
  const { data, error, isFetching } = useFetch(url)
  if (isFetching) {
    return 'Loading...'
  }
  if (error) {
    return <p>{error.message}</p>
  }
  if (!data) {
    return null
  }
  return data.map(({ name, html_url, description }) => (
    <div key={html_url}>
      <p>
        <a
          href={html_url}
          target="_blank"
          rel="noopener
noreferrer"
        >
          {name}
        </a>{' '}
      </p>
      <p>{description}</p>
    </div>
  ))
}

Un cambio de mejoras de legibilidad es manejar las promesas que retorna fetch usando la sintaxis de async await.

useEffect(() => {
  const doFetch = async () => {
    const fetchData = async () => {
      try {
        setIsFetching(true)
        const response = await fetch(url)
        const data = await response.json()
        setData(data)
      } catch (e) {
        setError(e)
      } finally {
        setIsFetching(false)
      }
      41
    }
    fetchData()
  }
  doFetch()
}, [url])

Hasta ahora estamos pasando por parámetro la url pero no estamos dando la opción de consumir endpoints que sean diferentes a GET.

Para poder personalizar mejor el fetch vamos a pasar un objeto como segundo parámetro en nuestro custom hook.

A este objeto le vamos a asignar propiedades con valores por default y las pasaremos al array de dependencias del useEffect para que pueda seguir funcionando apropiadamente.

const useFetch = (
  url,
  options = {
    method: 'GET',
    headers: new Headers(),
    mode: 'cors',
    cache: 'default',
    body: '',
  }
) => {
  const [data, setData] = useState(null)
  const [isFetching, setIsFetching] = useState(false)
  const [error, setError] = useState(null)
  const { method, headers, mode, cache, body } = options
  useEffect(() => {
    const doFetch = async () => {
      const fetchData = async () => {
        try {
          setIsFetching(true)
          const response = await fetch(url, {
            method,
            headers,
            mode,
            cache,
            body,
          })
          const data = await response.json()
          setData(data)
        } catch (e) {
          setError(e)
        } finally {
          setIsFetching(false)
        }
      }
      fetchData()
    }
    doFetch()
  }, [url, method, headers, mode, cache, body])
  return { data, error, isFetching }
}

¡Excelente! ya tenemos un hook que puedes usar en tus proyectos si lo deseas y hace sentido para ti.

Puedes consultar el código fuente de este custom hook que hemos creado en este enlace: https://codesandbox.io/s/fetch-with-hooks-iw9r9?file=/src/App.js.

Conclusión sobre Custom Hooks.

Este ha sido solo un ejemplo de lógica que se puede reusar entre componentes, pero existen muchas otras posibilidades.

Algo que podríamos mejorar en este ejemplo es desacoplar fetch y nuestro hook por medio de una segunda abstracción.

Pero lo que quiero resaltar aquí es el proceso de abstracción del manejo de estado en un componente y pasarlo a un custom hook.

Recuerda que toda lógica de estado que necesites implementar en varios componentes equivale a un custom hook en potencia a ser creado usando el mismo proceso de abstracción que hicimos en este ejemplo.

Te invito a que explores este repositorio (https://github.com/streamich/react-use) donde están publicados una gran cantidad de custom hooks creados por la comunidad como una inspiración para crear los tuyos propios.

Cosas por hacer:

Recursos adicionales

Si te ha gustado este análisis sobre el patrón Custom Hook, te invito a explorar los otros patrones avanzados en React JS que he analizado en la guía principal.

Y recuerda, si quieres llevar tus habilidades al siguiente nivel, no te pierdas el curso completo en el Canal de YouTube de Developero.