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.
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?
Aspectos a tener en cuenta para crear nuestros custom hooks
Como usar un hook correctamente
Primer custom hook en Qwik: useCounter
Custom Hook: useTheme
Custom Hook: useMousePosition
Custom Hook: useGeolocation
Más ideas para crear más custom hooks.
Conclusión.
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.
Son pocos los aspectos que hay que tener en cuenta pero son MUY IMPORTANTES. Os los dejo a continuación:
El nombre debe de empezar con la palabra use
. Esto sería una convención, más que una regla (aunque “hay que cumplirla”)
Se inician dentro de los componentes de Qwik, que estén implementados con la llamada a la función component$
. Está es una regla obligatoria.
Un hook puede llamar a otros hooks, sean los que vienen por defecto y los custom hooks. Detalle a tener en cuenta.
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:
Estado - Capítulo 8 - Parte 1: useSignal
y useStore
.
Estado computado - Capítulo 9 - Parte 2: useComputed$
Estado computado - Capítulo 11 - Consumo APIs: useResource$
Tareas y Ciclos de Vida - Capítulo 12: useTask$
y useVisibleTask$
use
en el nombreEsta 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.
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
.
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.
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.
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.
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>
<button onClick$={() => counter.value--}>-1</button>
<button onClick$={() => (counter.value = 0)}>Reset</button>
</>
);
});
+1
):-1
: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.
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:
Declarando un estado usando el hook useSignal
para almacenar el valor de contador. Con esto estaremos llamando a otro hook desde nuestro nuevo hook.
Crear las funciones serializadas (con $
, muy importante para serializar) para incrementar, decrementar y resetear el contador.
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> <button onClick$={() => changeValue(false)}>-1</button> <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 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.
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:
Crear estado mediante useSignal
para ir almacenando la variante del theme con el que estamos trabajando, cuyo valor por defecto es light
.
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.
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
.
Función para seleccionar el theme entre dark
y light
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.
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.
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.
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:
Importamos los elementos necesarios para el hook.
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
.
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
.
Función error que se ejecuta cuando navigator.geolocation.getCurrentPosition
da una respuesta NO satisfactoria, que se llamará en el apartado 7
.
Función geoFindMe que se encarga de realizar la geolocalización.
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
.
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.
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:
useLocalStorage
: Almacena y recupera datos en el almacenamiento local del navegador utilizando la API localStorage.
useWindowWidth
: Obtiene y actualiza el ancho de la ventana del navegador en tiempo real.
useDebounce
: Agrega un retardo antes de ejecutar una función, útil para implementar búsqueda con retardo o evitar ejecuciones innecesarias.
usePrevious
: Almacena el valor anterior de una variable o estado para realizar comparaciones o acciones basadas en cambios.
useClickOutside
: Detecta y maneja los clics fuera de un elemento específico, útil para cerrar menús o modales al hacer clic fuera de ellos.
useHover
: Detecta si el cursor está sobre un elemento, proporcionando un estado booleano para realizar acciones en función del estado de "hover".
useFetch
: Realiza una solicitud de datos a una API o servidor y maneja el ciclo de vida de la solicitud, incluyendo estados de carga, éxito y error.
useKeyPress
: Detecta la pulsación de una tecla específica o cualquier tecla del teclado y proporciona un estado booleano para realizar acciones en respuesta a la pulsación.
useToggle
: Alternar entre dos estados booleanos (verdadero/falso) y proporciona una función para cambiar el estado actual (prácticamente sería similar al useTheme, pero en este caso devolviendo un valor booleano).
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
Al finalizar este capítulo, deberíamos de ser capaces de:
custom hook
desde 0.custom hooks
donde usemos nuestra imaginación e integremos con otros hooks existentes como useSignal
, useStyles$
, etc.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.