Custom Hooks

Comenzamos con un nuevo capítulo en el que nos vamos a sumergir en un concepto que bajo mi punto de vista es muy importante conocer y dominar.

Vamos a explorar al detalle los custom hooks en Qwik, comprendiendo su estructura, su uso y los beneficios que aportan a nuestros proyectos.

Analizaremos los aspectos a tener en cuenta para crear de manera correcta nuestros custom hooks y posteriormente entraremos a la acción creando varios ejemplos prácticos y paso a paso de diferentes custom hooks, desde los más simples hasta los más avanzados, para que podamos comprender y aplicar esta poderosa técnica en nuestras propias aplicaciones.

En resumen, los custom hooks son una herramienta valiosa que nos permite construir aplicaciones más eficientes, reutilizables y fáciles de mantener en Qwik (En React también).

Con su capacidad para encapsular y compartir la lógica común, nos brindan un enfoque elegante para mejorar la modularidad y la legibilidad de nuestro código.

Contenido del capítulo

Estos serán los puntos que trataremos en este capítulo, como veís hay bastantes puntos e intentaremos tocar todos los aspectos fundamentales para poder entender bien todo lo relacionado a lo que estamos viendo en el capítulo.

¿Qué son los custom hooks en Qwik?

Los custom hooks en Qwik (como en React) son un tipo de función JavaScript que simula el funcionamiento de los hooks en Qwik.

Los custom hooks en Qwik son muy útiles siempre que tengamos una lógica que se repite entre varios componentes.

En estos casos, podemos sacar esta lógica y aplicarla a un custom hook, es decir, una función que ejecute los pasos que necesitamos de manera automática. ¿Qué beneficios obtenemos de realizar esta acción?

Al no ser funciones cualquiera, los custom hooks en Qwik deben seguir una serie de reglas para ser considerados hooks y no funciones. A continuación, os explicaré cuáles son.

Aspectos a tener en cuenta para crear nuestros custom hooks

Son pocos los aspectos que hay que tener en cuenta pero son MUY IMPORTANTES. Os los dejo a continuación:

Formato a seguir al definir nombre de la función

La primera regla de los custom hooks en Qwik es que su nombre debe empezar con la palabra use.

Esta convención se crea siguiendo los hooks originales de Qwik.

Hasta ahora hemos trabajado con algunos de ellos en capítulos anteriores (aunque hay más que podéis encontrar en la documentación oficial) que añadiré sus referencias por los apartados que vienen a continuación:

Sobre use en el nombre

Esta regla, la del nombre que debe de empezar con use es utilizada en React, por lo que si venís a Qwik teniendo unos conocimientos sólidos de React, prácticamente este paso ya lo tenéis más que asimilado.

Se considera que esto es una regla porque la comunidad ha decidido que es más sencillo reconocer cualquier hook (por defecto o custom)cuando sigue esta convención.

Esto se estableció en React y se ha implementado también en Qwik, para que sea más fácil trabajar siguiendo las convenciones de la comunidad.

Eso si, en teoría podríamos crear un custom hook con otro nombre (sin el use) sin que nos diese errores ni problemas, pero no es lo recomendable, por lo que vamos a procurar seguir las recomendaciones y reglas establecidas con el objetivo de aplicar las mejores prácticas.

Debe de estar englobado dentro de la función component$ {#inside-component-dollar-add}

Esta SI es una regla obligatoria, ya que si no implementamos la ejecución de un hook dentro de un componente de Qwik que realiza la llamada a la función component$ nos va a dar un error en el que básicamente nos dirá:

Code(20): Calling a 'use*()' method outside 
'component$(() => { HERE })' is not allowed. 
'use*()' methods provide hooks to the 'component$' state and lifecycle, 
ie 'use' hooks can only be called synchronously within the 'component$' 
function or another 'use' method.
For more information see: https://qwik.builder.io/docs/components/tasks/#use-

Esto es debido a que los métodos use*() proporcionan hooks al estado y ciclo de vida de component$, es decir, los hooks solo se pueden llamar de forma síncrona dentro de la función component$ o en otro método use.

Puede llamar a otros hooks

No es una regla, pero es algo que tenemos que tener en cuenta de manera particular en los custom hooks de Qwik (como en React) en el que podríamos llamar a otros hooks.

En este caso, Qwik se considera como custom hook a aquella función en la que dentro de ella llama a un hook original o a otro custom hook que hemos creado.

Teniendo en cuenta estos aspectos, os muestro a continuación lo que hay que tener en cuenta para aplicar bien el uso de los hooks tanto los que vienen de serie (useSignal, useStore,…), como los custom hooks.

Cómo usar un hook correctamente

A continuación, os dejo la formas incorrectas / correctas que tenemos para poder hacer uso de los hooks en Qwik.

// <-- ❌ No funcionará por estar fuera de component$
useHook(); 
     
export default component$(() => {
  // <-- ✅ Dentro de component$ y en la raíz
  useCustomHook(); 

  if (condition) {
    // <-- ❌ Aunque esté dentro de component$, 
    // no está en la raíz
    useHook(); 
  }

  useTask$(() => {
    // <-- ❌ No podemos usar un hook dentro de otro
    useNavigate(); 
  });

  // <-- ❌ Debe de estar en la raíz
  const myQrl = $(() => useHook()); 

  // <-- ❌ No se puede usar con acciones
  return <button onClick$={() => useHook()}></button>; 
});
    
  // Dentro de una función denominada custom hook funciona como en component$
  function useCustomHook() {
    // <-- ✅ Funciona por estar en raíz
    useHook(); 

    if (condition) {
      // <-- ❌ No está en la raíz, está dentro de condición
      useHook(); 
    }
  }

Esta porción de código os recomiendo que la tengáis siempre a mano mientras estéis aprendiendo. Luego, cuando ya tengáis todo bien interiorizado, inconscientemente lo aplicaréis perfectamente sin darle muchas vueltas.

Primer custom hook en Qwik: useCounter

A continuación, os voy a mostrar como podemos crear un custom hook paso a paso, desde el desarrollo de un componente, donde aplicaremos la lógica necesaria para darle funcionamiento.

Una vez comprobado que todo funciona perfecto, creamos el custom hook (no olvidéis que hay que respetar el uso de la palabra use*() aunque no sea obligatoria para su funcionamiento) desacoplando ese código del componente principal para poder reutilizarlo en otros componentes tantas veces como queramos.

Paso 1: Crear la lógica de un contador

Escribiremos el siguiente código, donde implementamos un valor mediante un useSignal, para almacenar el estado de el.

Posteriormente, aplicamos ese valor en la parte del layout, para que se visualice junto dos botones de acción, para ir sumando +1 y el otro para hacer un reset del estado del contador y ponerlo a 0.

import { component$, useSignal, useStyles$ } from '@builder.io/qwik';

export default component$(() => {
  useStyles$(`
    .current-value {
      font-size: x-large
    }
  `);

  const counter = useSignal(0);

  return (
    <>
      <h1>Contador - sin usar custom hook</h1>
      <p>Ejecuta una de las tres opciones para sumar (+1), restar (-1) o hacer un reset (0)</p>
      <p>Valor Actual : <span class="current-value">{counter.value}</span></p>
      <button onClick$={() => counter.value++}>+1</button>&nbsp;
      <button onClick$={() => counter.value--}>-1</button>&nbsp;
      <button onClick$={() => (counter.value = 0)}>Reset</button>
    </>
  );
});

Casos de uso

  1. Esto se verá de la siguiente forma (tres veces click +1):
  1. Una vez click -1:
  1. Reset:

Podéis probar la lógica, funciona sin problemas con más variantes.

Ahora bien, queremos que esa lógica, la de tener el valor del contador, y las funciones que corresponden a +1, -1 y el reset, las queremos quitar del componente.

Paso 2: Crear hook useCounter

Teniendo la lógica implementada en el componente, necesitamos los tres elementos principales, creando un custom hook(en src/hooks llamando como useCounter.tsx) siguiendo lo mencionado anteriormente:

import { useSignal, $ } from '@builder.io/qwik';

const useCounter = (initialValue = 0) => {
  // 1
  const counter = useSignal(initialValue);

  // 2
  const changeValue = $((increment: boolean) => {
    if (increment) {
        counter.value++;
        return;
    }
    counter.value--;
  });
  const reset = $(() => (counter.value = 0));

  // 3
  return {
    counter,
    changeValue,
    reset,
  };
};

export {useCounter};

Como podemos ver en las líneas de código anteriores, nuestra función se llama useCounter, siguiendo la primera regla de los custom hooks.

Y dentro de el, estamos aplicando lo siguiente:

  1. Declarando un estado usando el hook useSignal para almacenar el valor de contador. Con esto estaremos llamando a otro hook desde nuestro nuevo hook.

  2. Crear las funciones serializadas (con $, muy importante para serializar) para incrementar, decrementar y resetear el contador.

  3. Devolvemos tanto el estado como las funciones, para poder usarlas en los componentes que queramos

Y ahora usando el custom hook dentro del componente, lo dejamos de la siguiente forma:

import { component$, useStyles$ } from '@builder.io/qwik';

// 1.- Importamos el custom hook
import {useCounter} from '~/hooks/useCounter';

export default component$(() => {
  useStyles$(`
    .current-value {
      font-size: x-large
    }
  `);
  // 2.- Iniciamos el custom hook
  const {changeValue, reset, counter} = useCounter(0);
  return (
    <>
      <h1>Contador - usando hook useCounter</h1>
      <p>Ejecuta una de las tres opciones para sumar (+1), restar (-1) o hacer un reset (0)</p>
      <p>Valor Actual: <span class="current-value">{counter.value}</span></p>
      <p><button onClick$={() => changeValue(true)}>+1</button> &nbsp; <button onClick$={() => changeValue(false)}>-1</button> &nbsp; <button onClick$={reset}>RESET</button></p>
    </>
  );
});

Si guardamos los cambios, seguiremos con la misma apariencia del contador y seguirá funcionando de la misma forma.

Lo que ahora cambia respecto a lo anterior es que ahora en vez de tener la lógica dentro del componente, la tendremos fuera, con lo que nos dará más libertad para usarlo en otros componentes (quitando la duplicidad de códigos repetitivos) y en el caso de realizar cambios, que todos estos reciban dichos cambios en el momento.

Probad con diferentes acciones

Probad incrementando, decrementando y reseteando los valores, ya veréis que funciona perfecto y de la misma forma que en el punto anterior.

En resumen, podemos afirmar que los custom hooks en Qwik (y React) son muy útiles para extraer funcionalidades, hacer refactorizaciones de código y mantener nuestros componentes más simplificados.

Ahora que ya sabemos como trabajar en el desarrollo de un custom hook, a continuación os voy a mostrar varios ejemplos en diferentes variantes haciendo distintas utilidades como puede ser un selector de temas (dark / light), un hook que detecta la posición de nuestro ratón en la ventana activa del navegador junto con otros ejemplos que vamos a ver.

Custom Hook: useTheme

Este custom hook lo que va a hacer es que mediante una opción de tipo toggle nos proporciona la opción de seleccionar la apariencia de nuestra plantilla de clara a oscura y viceversa, con las variantes light y dark.

Creamos el hook en src/hooks con el nombre useTheme.tsx y añadimos el siguiente código:

import { useSignal, $, useVisibleTask$, useStyles$ } from '@builder.io/qwik';

const useTheme = () => {
  // 1
  const theme = useSignal('light');

  // 2
  useStyles$(`
    .dark {
      background: #1d2033;
      color: white;
    }
    .light {
      background: white;
      color: black;
    }
  
    .light, .dark, body {
      padding: 1rem;
    }
  `);

  // 3
  useVisibleTask$(({track}) => {
    track(() => theme.value);
    if (document) {
        // Eliminamos las dos clases antes de asignarle la elegida
        document.body.classList.remove('dark', 'light');
        // Añadimos la variante seleccionada
        document.body.classList.add(theme.value);
    }
  })

  // 4
  const toggleTheme = $(() => {
    theme.value = theme.value === 'light' ? 'dark' : 'light';
  });

  // 5
  return {
    theme,
    toggleTheme,
  };
};

export { useTheme };

Y en este código los aspectos a tener en cuenta son los siguientes:

  1. Crear estado mediante useSignal para ir almacenando la variante del theme con el que estamos trabajando, cuyo valor por defecto es light.

  2. Uso de otro hook, en este caso estilos personalizados con useStyles$() para que estos estilos se apliquen globalmente a todo el proyecto tal y como hemos visto en el Estilos.

  3. Uso de otro hook donde en este caso usamos useVisibleTask$ para gestionar la clase que se le asigna al body teniendo en cuenta el valor actual de theme que estamos observando mediante la función track.

  4. Función para seleccionar el theme entre dark y light

  5. Devolver el valor del theme y la función para hacer el cambio de variante.

Y ahora, añadiéndolo en un componente:

import { component$ } from '@builder.io/qwik';
import {useTheme} from '~/hooks/useTheme';

export default component$(() => {
  const { theme, toggleTheme } = useTheme();

  return (
    <div>
      <h4>Hook useTheme</h4>
      <p>Tema seleccionado: {theme.value}</p>
      <button onClick$={toggleTheme}>Cambiar</button>
    </div>
  );
});

Cuya apariencia será la siguiente, que por defecto será la apariencia light, la de tono más claro, que podremos cambiar a una variante más oscura (dark) con la opción Cambiar:

Si abrimos el Inspector de elementos de nuestro navegador podemos observar que se está aplicando el estilo light en el selector body aplicando el estilo definido en el custom hook:

Cambiamos a la apariencia oscura, es decir a la variante dark:

Y observando en el Inspector de elementos como hemos realizado anteriormente, ahora podemos observar que la clase aplicada a body es la clase dark que da este estilo más oscuro:

Llegados a este punto, podríamos decir que ya tenemos otro custom hook. Vamos a por el siguiente.

Custom Hook: useMousePosition

Vamos a crear un hook, en el que ya estemos haciendo uso del hook useOnDocument, para poder obtener mediante un evento de movimiento del ratón, la posición actual dentro de nuestro documento web, lo que sería toda la ventana.

Creamos el hook en src/hooks con el nombre useMousePosition.tsx y añadimos el siguiente código:

import { useStore, $, useOnDocument } from '@builder.io/qwik';
    
const useMousePosition = () => {
  // 1.- Guardamos la posición del cursor del ratón con sus coordenadas
  const position = useStore({ x: 0, y: 0 });

  // 2.- Escuchar eventos en el elemento raíz del componente actual.
  useOnDocument(
    'mousemove',
    $((event) => {
      // 3.- Obtenemos el evento del ratón
      const { x, y } = event as MouseEvent;
      // 4.- Actualizamos
      position.x = x;
      position.y = y;
    })
  );
  return position;
};

export {useMousePosition};

Y una vez definido el custom hook, lo añadimos en el componente:

import { component$ } from '@builder.io/qwik';
import {useMousePosition} from '~/hooks/useMousePosition';

export default component$(() => {
  // 1.- Uso de nuestro hook para detectar la posición del cursor
  const pos = useMousePosition();
  // 2.- Mostrar las coordenadas x, y
  return (
    <div>
      MousePosition: ({pos.x}, {pos.y})
    </div>
  );
});

Y una vez cargado el componente, veremos como cambian los valores de x e y si movemos el cursor del ratón en lo que es el documento de nuestra página.

Este hook no hace nada más que mostrar la posición del cursor del ratón, es bastante simple, pero se ve útil para practicar lo visto en el capítulo Eventos.

Pasamos al siguiente hook, donde haremos una nueva funcionalidad para obtener nuestra ubicación mediante la API Geolocation de nuestro navegador.

Custom Hook: useGeolocation

Después de haber trabajado con el custom hook anterior, empezamos a trabajar con un hook que realmente me parece super útil y que seguramente se vaya a utilizar un montón, sobre todo en situaciones donde necesitemos obtener la ubicación de la persona que está navegando para sugerirle información relativa a su ubicación.

¿Qué información se podría sugerir en base a una ubicación?

En estos casos, podríamos especificar a cuantos kms a la redonda queremos filtrar y tendríamos resultados cercanos, haciendo que sea super útil este hook que vamos a crear.

Documentación oficial API GeoLocation

Para crear el custom hook vamos a usar esta referencia:
https://shorten-up.vercel.app/SN_VgPihgG

Cualquier cambio que se pueda dar en el futuro, deberíamos de acudir a esa referencia para hacer los ajustes necesarios en nuestros `custom hook`.

Para empezar con el custom hook llamado useGeolocation, creamos el fichero useGeolocation.tsx dentro del apartado src/hooks, tal y como hemos hecho anteriormente, que sirve para obtener la ubicación actual del usuario a través de la API de geolocalización del navegador

Y una vez creado, añadimos el siguiente código:

// 1
import { useSignal, $ } from '@builder.io/qwik';

const useGeolocation = () => {
  // 2
  const location = useSignal<{
    latitude?: number;
    longitude?: number;
    error: string;
    text: string;
  }>({
    latitude: 0,
    longitude: 0,
    error: '',
    text: '',
  });

  // 3
  const success = $(
    (position: { coords: { latitude: number; longitude: number } }) => {
      location.value = {
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
        error: '',
        text: `Position found in ${position.coords.latitude}, ${position.coords.longitude}`,
      };
    }
  );

  // 4
  const error = $(() => {
    location.value.error = 'Unable to retrieve your location';
  });

  // 5
  const geoFindMe = $(() => {
    // 6
    if (!navigator.geolocation) {
      location.value = {
        latitude: undefined,
        longitude: undefined,
        error: 'Geolocation is not supported by your browser',
        text: ``,
      };
      return;
    }
    // 7
    location.value = {
      latitude: undefined,
      longitude: undefined,
      error: '',
      text: `Locating...`,
    }; 
    navigator.geolocation.getCurrentPosition(success, error);
  });


  return {
    geoFindMe,
    location
  };
};

export { useGeolocation };

Y en este código los aspectos a tener en cuenta son los siguientes:

  1. Importamos los elementos necesarios para el hook.

  2. Iniciamos el elemento location donde tendremos almacenada la información de la ubicación (latitud y longitud) junto con el estado de los textos de error y mensaje satisfactorio con text.

  3. Función success que se ejecuta cuando navigator.geolocation.getCurrentPosition da una respuesta satisfactoria almacenando su posición actual, que se llamará en el apartado 7.

  4. Función error que se ejecuta cuando navigator.geolocation.getCurrentPosition da una respuesta NO satisfactoria, que se llamará en el apartado 7.

  5. Función geoFindMe que se encarga de realizar la geolocalización.

  6. Comprueba si el navegador NO tiene compatibilidad con la funcionalidad de la Geolocalización. En el caso de NO tenerla se asigna a la propiedad error el mensaje Geolocation is not supported by your browser.

  7. Si es compatible el navegador, ejecuta la función navigator.geolocation.getCurrentPosition para obtener una respuesta o bien satisfactoria o erronea, por algún motivo cualquiera.

Y ahora, añadiéndolo en un componente:

import { component$ } from '@builder.io/qwik';
import { useGeolocation } from '~/hooks/useGeolocation';

export default component$(() => {
  // 1.- Uso de nuestro hook para detectar nuestra posición
  const { geoFindMe, location } = useGeolocation();
  // 2.- Posición latitud / longitud después de darle permisos
  return (
    <div>
      <p>
        Posición: ({location.value.latitude}, {location.value.longitude})
      </p>
      <p>
        {location.value.text !== ''
          ? location.value.text
          : location.value.error !== ''
          ? location.value.error
          : ''}
      </p>
      <button onClick$={geoFindMe}>Busca donde estoy</button>
    </div>
  );
});

Con esto, si guardamos los cambios, ya tenemos disponible el poder obtener nuestra localización.

Para obtener dicha localización, pulsamos Busca donde estoy.

Si NO tenemos otorgados los permisos al navegador, nos aparecerá un mensaje para que confirmemos que proporcionamos los permisos para acceder a nuestra ubicación:

Para obtener la ubicación pulsamos en Permitir. Obviamente, si seleccionamos Bloquear, nunca nos proporcionará nuestra ubicación.

Una vez que seleccionamos permitiendo el acceso, en unos pocos segundos obtenemos nuestra ubicación:

Y llegados a este punto, hemos completado el ejemplo de otro hook.

Ahora podremos hacer de ello y cualquier cambio que sufra la API, habrá que aplicarlo únicamente en src/hooks/useGeolocation.tsx, independientemente de cuantas veces se usa en el proyecto, consiguiendo que sea mucho más fácil de mantener, tal y como ocurriría con cualquier otro hook o custom hook usado.

Ideas de Custom Hooks

En este punto, no os enseñaré a crear más custom hooks, pero os voy a proporcionar algunos ejemplos de custom hooks que podríais crear con lo que hemos aprendido y siguiendo la información de la documentación oficial:

Resultado de lo trabajado en este capítulo

El código que encontráis es el resultado final de todo el proceso realizado durante el capítulo. Os recomiendo que vayáis haciendo los pasos poco a poco y paso a paso para ir interiorizando todos los conceptos y en este caso, pienso que os vendría bien realizar pruebas con más ejemplos.

El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/bdvG941Z4i

¿Qué hemos aprendido en este capítulo?

Al finalizar este capítulo, deberíamos de ser capaces de:

Conclusion

Los custom hooks son una característica poderosa en Qwik (y React) que nos permitirán encapsular y reutilizar la lógica común en nuestros componentes. Al separar la lógica en hooks personalizados, podemos mantener nuestros componentes más limpios, centrados en la presentación y fáciles de entender. Esto mejora la reutilización del código, facilita el mantenimiento y agiliza el desarrollo de nuevas funcionalidades.

Los custom hooks nos van a brindar flexibilidad y versatilidad al permitirnos abordar una amplia gama de necesidades en nuestras aplicaciones, desde el manejo del estado y los efectos secundarios hasta la interacción con APIs externas. Al utilizar custom hooks, podemos compartir y aplicar fácilmente la misma lógica en múltiples componentes, evitando la duplicación de código y mejorando la eficiencia del desarrollo.

En definitiva, los custom hooks nos dan un poder y control muy grande como desarrolladores de Qwik al mejorar la modularidad, la legibilidad y la reutilización del código. Al utilizar esta técnica, podemos construir aplicaciones más eficientes y escalables, lo que nos permitirá enfrentarnos a los desafíos del desarrollo web con mayor confianza y agilidad.