Qwik — Hook useComputed$
Claves para aprender a trabajar este hook que nos permite hacer un tracking por defecto y encima retornar un valor
Comenzamos con un nuevo artículo en el que vamos a aprender a hacer uso del hook useComputed$()
función que nos va a facilitar mucho el trabajo cuando trabajamos con valores computados.
Os recomiendo leer los artículos anteriores en orden y si practicáis, ¡¡mucho mejor!!.
Recordad, que si hay dudas, aun leyéndolo, podéis preguntarlo en los comentarios y sin dudas me gustaría que me comentéis también, para tener feedback.
Todos los artículos publicados del curso los encontraréis en la siguiente lista que iré actualizando semanalmente y estableciendo el orden natural recomendado:
La versión usada para el artículo es la versión 0.104.0 de Qwik (2023–05–01), puede que hayan cambios y por eso, si hubiese algo que consultar, os recomiendo ir a la documentación oficial.
Junto con lo que vamos a ver, necesitamos tener claro como funcionan tanto useSignal()
como useStore()
para gestionar el estado de una aplicación. Estos conceptos los explico en este artículo que podréis acceder de manera GRATUITA.
Contenido del artículo
- Introducción
- Uso básico y primeros pasos
- Convertir una cadena de texto a mayúsculas.
- Combinar el nombre y apellidos introducidos en un nuevo texto.
- Mostrar opciones activas de lista de opciones con checked
Introducción
Para empezar a trabajar con ello, tendremos que tener claros los conceptos de estas dos preguntas: ¿Qué es useComputed$()
? ¿Qué hace?
useComputed$()
es la forma preferida de crear valores computados que nos permite memoizar un valor derivado sincrónicamente de otro estado.
Es similar a memo
en otros frameworks, ya que solo volverá a calcular el valor cuando reciba señales de cambio en la entrada.
Ejemplos que podríamos mencionar, serviría para crear una versión en minúsculas de una cadena o combinar el nombre y apellido en un nombre completo, entre otros ejemplos que veremos en este artículo.
Uso básico y primeros pasos
En este apartado os enseño lo básico, desde lo que es la importación de la función hasta añadirlo en componente de la ruta donde trabajaremos.
Imaginaros que estamos en la ruta de raíz, es decir, en src/routes/index.tsx
con el siguiente contenido:
import { component$ } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
export default component$(() => {
return (
<>
<div class="section bright">
<div class="container center">
</div>
</div>
</>
);
});
export const head: DocumentHead = {
title: 'Welcome to Qwik',
meta: [
{
name: 'description',
content: 'Qwik site description',
},
],
};
Cuyo contenido se visualizará de la siguiente forma (o similar, puede cambiar con la versiones el apartado de estilos):
Para poder usar useComputed$()
debemos de importarlo de la siguiente forma:
import { useComputed$ } from '@builder.io/qwik';
También añadiremos useSignal()
que servirá para definir el estado inicial:
import { useSignal } from '@builder.io/qwik';
Quedando de la siguiente forma el apartado del import:
import { component$, useComputed$, useSignal } from '@builder.io/qwik';
Ahora nos centramos en iniciar un valor numérico entero junto con la opción de useComputed$()
para ir obteniendo el valor pero duplicado por 2. Primero añadimos los dos valores:
import { component$, useComputed$, useSignal } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
export default component$(() => {
const valueCounter = useSignal(0);
// esto se ejecuta cada vez valueCounter.value sufre cambios y
// multiplicará por 2
const doubleValueCounter = useComputed$(() => valueCounter.value * 2);
return (
<>
<div class="section bright">
<div class="container center">
<p>
Valor con <code>useSignal</code>: {valueCounter.value}
</p>
<p>
Valor con <code>useComputed$</code>: {doubleValueCounter.value}
</p>
</div>
</div>
</>
);
});
Y lo que se muestra es el siguiente resultado, en los dos tenemos el valor 0.
Tenemos que tener en cuenta ahora lo siguiente:
// esto se ejecuta cada vez valueCounter.value sufre cambios y
// multiplicará por 2
const doubleValueCounter = useComputed$(() => valueCounter.value * 2);
Dentro de esta función, estamos realizando la operación de transformación con el nuevo valor computado, que sería en este caso 0, teniendo 0 como valor de valueCounter.value
.
¿Qué pasará si le asignamos un 1 a valueCounter
e iniciamos la página?
export default component$(() => {
const valueCounter = useSignal(1); // <===== El cambio a 1
...
);
});
Debería de mostrar en valueCounter.value
= 1 y en dobleValueCounter.value
= 2 por ser el doble.
Bien, ya vemos que cambia el valor, pero vamos a hacer que tenga más dinamismo y que podamos modificarlo con un click de botón mediante el evento onClick$
aplicando un color rojo de fondo, para que se vea y podamos trabajar con el:
export default component$(() => {
useStyles$(`
button {
background: red;
}
`);
...
return (
<>
<div class="section bright">
<div class="container center">
...
<button onClick$={() => valueCounter.value++}> + 1</button>
</div>
</div>
</>
);
});
Quedando de la siguiente forma:
Ahora si hacemos click 4 veces, tendremos el primer valor con 5 y el segundo como es el doble, será 10:
Bien, el primer ejemplo ya tenemos. Tenemos ya las primeras nociones para pasar a otro ejemplo con el objetivo de reforzar lo aprendido en este punto.
Convertir una cadena de texto a mayúsculas.
Este apartado será prácticamente igual al anterior pero en vez de trabajar con números vamos a trabajar con datos de tipo string. Podemos considerar este punto como un extra de refuerzo, para asentar lo aprendido en el punto anterior.
Ahora lo que vamos a tener es un valor que irá convirtiendo a mayúsculas a medida que cambiemos el estado en el valor original.
Vamos a imaginarnos que tenemos un array de varios nombres:
const namesList = ['anartz', 'ruslan', 'bezael', 'leifer mendez'];
Y que el valor asignado al useSignal
, sea la posición seleccionada, para que cada vez que hacemos click haga un +1 a la posición index hasta llegar al 3 para volver a asignarse el 0 y así sucesivamente.
Aplicamos esos cambios dejando el código de la siguiente forma:
import {
component$,
useComputed$,
useSignal,
useStyles$,
} from '@builder.io/qwik';
export default component$(() => {
useStyles$(`
button {
background: red;
}
`);
const namesList = ['anartz', 'ruslan', 'bezael', 'leifer mendez'];
const indexSelect = useSignal(0);
// esto se ejecuta cada vez valueCounter.value sufre cambios
const nameSelectUppercase = useComputed$(() => indexSelect.value);
return (
<>
<div class="section bright">
<div class="container center">
<p>
Valor con <code>useSignal</code>: {namesList[indexSelect.value]}
</p>
<p>
Valor con <code>useComputed$</code>: {nameSelectUppercase.value}
</p>
<button
onClick$={() => {
indexSelect.value =
indexSelect.value === namesList.length - 1
? 0
: indexSelect.value + 1;
}}
>
{' '}
+ 1
</button>
</div>
</div>
</>
);
});
Y se visualizará de esta forma:
Donde en 1 tenemos el valor de seleccionar el valor índice (indexSelect.value
= 0) de nameList
que será el primer nombre y en el 2 muestra el valor actual de indexSelect.value
.
Cuando llegue a
indexSelect.value
=== 3, resetea a 0 para poder estar visualizando los nombres todo el tiempo.
Bien, ahora lo que tenemos que hacer es la conversión en el valor computado dentro de useComputed$()
que se asigna al valor nameSelectUppercase
// esto se ejecuta cada vez valueCounter.value sufre cambios
const nameSelectUppercase = useComputed$(() => indexSelect.value);
Cambiamos indexSelect.value
por la transformación del texto seleccionado usando toUpperCase()
const nameSelectUppercase = useComputed$(() => namesList[indexSelect.value].toUpperCase());
Automáticamente al guardar, ya se inicia el valor computado en base a la posición seleccionada y esto será lo que se verá:
Si pulsamos + 1, iremos viendo los diferentes nombres con useSignal()
y useComputed$()
donde se verá en el primero el valor original en minúsculas y en el segundo caso en mayúsculas completamente.
Ahora que ya hemos trabajado con la combinación useSignal()
y useComputed$()
, pasamos al siguiente apartado donde ya vamos a trabajar con la gestión del estado mediante elemento más complejos, que haremos uso mediante useStore()
.
Combinar el nombre y apellidos introducidos en un nuevo texto
Comenzamos con un nuevo apartado donde seguiremos trabajando con useComputed$()
pero combinándolo obteniendo los cambios a partir del useComputed$()
en vez de useSignal()
, eso si, mantenemos el valor de la selección del index (indexSelect
) para ir seleccionando los nombres mediante el botón de +1
.
Sabiendo lo anterior, el primer paso será ampliar el array anterior añadiendo en vez de valores de tipo string, añadiremos objetos con dos propiedades, name
y lastname
para especificar los nombres y apellidos.
Tenemos actualmente esto:
const namesList = ['anartz', 'ruslan', 'bezael', 'leifer mendez'];
Pasamos a lo siguiente:
const namesList = [
{ name: 'Anartz', lastname: 'Mugika' },
{ name: 'Ruslan', lastname: 'González' },
{ name: 'Bezael', lastname: 'Pérez'},
{ name: 'Leifer', lastname: 'Mendez'}
];
Ahora añadimos el apartado para computar los cambios en el index
mediante el botón de +1
que hemos visto antes donde cogerá el valor name
y el valor lastname
del elemento seleccionado y devuelve un string.
// esto se ejecuta cada vez indexSelect.value sufre
// cambios para concatenar esos dos valores en uno
const nameLastnameTtext = useComputed$(
() =>
`${namesList[indexSelect.value].name} ${
namesList[indexSelect.value].lastname
}`
);
Y el código se queda así, haciendo también las adaptaciones en el código JSX que hace referencia al nuevo valor computado:
import {
component$,
useComputed$,
useSignal,
useStyles$,
} from '@builder.io/qwik';
export default component$(() => {
useStyles$(`
button {
background: red;
}
`);
// Nueva lista
const namesList = [
{ name: 'Anartz', lastname: 'Mugika' },
{ name: 'Ruslan', lastname: 'González' },
{ name: 'Bezael', lastname: 'Pérez' },
{ name: 'Leifer', lastname: 'Mendez' },
];
const indexSelect = useSignal(0);
// esto se ejecuta cada vez indexSelect.value sufre cambios
const nameLastnameText = useComputed$(
() =>
`${namesList[indexSelect.value].name} ${
namesList[indexSelect.value].lastname
}`
);
return (
<>
<div class="section bright">
<div class="container center">
<p>
Valor índice para seleccionar persona con <code>useSignal</code>:{' '}
{indexSelect.value}
</p>
<p>
Valor con <code>useComputed$</code>: {nameLastnameText.value}
</p>
<button
onClick$={() => {
indexSelect.value =
indexSelect.value === namesList.length - 1
? 0
: indexSelect.value + 1;
}}
>
{' '}
+ 1
</button>
</div>
</div>
</>
);
});
Guardando los cambios, se verá de la siguiente forma, muy similar a antes con la diferencia que el resultado es la combinación de name
+ lastname
.
Ahora que ya hemos trabajado con ello, vamos a añadir un useStore()
para jugar con ello e ir almacenando el valor de la persona seleccionada.
Añadimos el contenedor infoDataStore
donde metemos la lista de personas y la selección (eliminamos namesList
) de una de ellas:
import {
useStore,
} from '@builder.io/qwik';
...
const infoDataStore = useStore({
list: [
{ name: 'Anartz', lastname: 'Mugika' },
{ name: 'Ruslan', lastname: 'González' },
{ name: 'Bezael', lastname: 'Pérez' },
{ name: 'Leifer', lastname: 'Mendez' },
],
select: { name: 'Anartz', lastname: 'Mugika' },
});
Y modificamos la lógica de la acción del click, para modificar el estado de la propiedad select del contenedor infoDataStore
. Pasamos de esto:
<button
onClick$={() => {
indexSelect.value =
indexSelect.value === namesList.length - 1
? 0
: indexSelect.value + 1;
}}
>
{' '}
+ 1
</button>
A lo siguiente:
<button
onClick$={() => {
// Seleccionamos primero el índice
indexSelect.value =
indexSelect.value === infoDataStore.list.length - 1
? 0
: indexSelect.value + 1;
// Almacenamos el valor seleccionado de la lista de personas
// Esto hará que notifique el cambio en infoDataStore
infoDataStore.select = infoDataStore.list[indexSelect.value];
}}
>
{' '}
+ 1
</button>
Y ahora modificamos el apartado donde se computa el valor nameLastnameText
donde vamos a usar ya lo almacenado en la propiedad select de infoDataStore
para hacer la transformación anterior:
const nameLastnameText = useComputed$(
() =>
// YA NO EXISTE namesList y ahora queremos usar infoDataStore.select
`${namesList[indexSelect.value].name} ${
namesList[indexSelect.value].lastname
}`
);
Al siguiente código:
const nameLastnameText = useComputed$(
() =>
`${infoDataStore.select.name} ${
infoDataStore.select.lastname
}`
);
Como podéis observar, ahora ya como no usa el useSignal()
anterior del índice y ya trabajamos con el useStore()
, es más que suficiente que seleccionemos con infoDataStore
y su propiedad select para tener el elemento y así posteriormente concatenar las propiedades name
y lastname
como hemos realizado anteriormente.
El resultado será el mismo que lo que hemos implementado antes de este paso.
Ahora ya después de ver los casos básicos y no tan reales, vamos pasando al siguiente nivel, donde nos vamos a centrar en una lista de opciones con selección de activo o no de las opciones que ya se asemejará más a usos reales.
Mostrar opciones activas de lista de opciones con checked
Vamos a almacenar una lista de filtros, con las propiedades de label y checked para que las opciones apareazcan como un pequeño resumen que se actualizará en base a los cambios realizados en las opciones de la lista que se añadirá a continuación.
Con ello, lo que vamos a hacer es que lo valores activos, en el momento tenga un fondo verde y los no activos, rojo (o nada, según como os guste) y a su vez, con el valor computado, podamos obtener una lista de hobbies que serán los activos.
Vamos a almacenar algunos valores de dentro de un contenedor mediante useStore
:
const myHobbiesList = useStore({
options: [
{
label: 'Running',
checked: true,
},
{
label: 'Trail Running',
checked: true,
},
{
label: 'Football',
checked: false,
},
{
label: 'Gym',
checked: true,
},
{
label: 'Basketball',
checked: false,
},
{
label: 'Handball',
checked: true,
},
],
});
Donde cada una de las opciones, mediante el valor checked
controlaremos el estado de activo / no activo e ir viendo como se modifica la información mediante la acción de click en los botones.
Lo primero, lo que vamos a hacer es añadir el valor que estará escuchando los cambios que se darán en myHobbiesList
usando useComputed$()
.
import { component$, useStyles$, useStore, useComputed$ } from '@builder.io/qwik';
export default component$(() => {
useStyles$(`
button {
background: red;
}
`);
const myHobbiesList = useStore({
options: [
...
],
});
// Primero filtra los activos y luego obtiene solo los labels de esos resultados
const mySelectHobbies = useComputed$(() =>
myHobbiesList.options
.filter((item) => item.checked)
.map((item) => item.label)
);
return (
<>
<div class="section bright">
<div class="container center">
{myHobbiesList.options.map((value, index) => (
<button
key={value.label}
class={value.checked ? 'checked' : 'no-checked'}
>
{value.label}
</button>
))}
<h4>Mis hobbies actuales</h4>
{JSON.stringify(mySelectHobbies.value)}
</div>
</div>
</>
);
});
Y esto es lo que se verá actualmente, que comparando con el valor inicial, corresponde la lista a los que hemos dicho que estaba seleccionados:
Ahora eliminamos todo el contenido que tenemos en la parte donde se añade el código JSX el contenido para mostrar los botones con el evento onClick$()
quedando de la siguiente forma el código:
import { component$, useStyles$, useStore } from '@builder.io/qwik';
export default component$(() => {
useStyles$(`
button {
background: red;
}
`);
const myHobbiesList = useStore({
options: [
...
],
});
const mySelectHobbies = useComputed$(() =>
...
);
return (
<>
<div class="section bright">
<div class="container center">
{myHobbiesList.options.map((value, index) => (
<button
key={value.label}
class={value.checked ? 'checked' : 'no-checked'}
onClick$={() =>
console.log(
'Con esto cambiaremos ',
myHobbiesList.options[index]
)
}
>
{value.label}
</button>
))}
<h4>Mis hobbies actuales</h4>
{JSON.stringify(mySelectHobbies.value)}
</div>
</div>
</>
);
});
Mostrar elementos de una lista en base a los filtros aplicados en el momento.
Quedando de la siguiente forma:
Y haciendo click en orden de 0 a 2:
Como se puede observar, está cogiendo correctamente la posición del botón que estamos haciendo click.
Lo que vamos a hacer es analizar las clases, ya que los elementos que el valor de la propiedad checked es true, tendrán la clase checked
y lo que no, serán no-checked
.
Abrimos el inspector de elementos y analizamos si las clases están bien asignadas dependiendo de lo especificado en el myHobbiesList
anteriormente:
const myHobbiesList = useStore({
options: [
{ label: 'Running', checked: true},
{ label: 'Trail Running', checked: true},
{ label: 'Football', checked: false },
{ label: 'Gym', checked: true },
{ label: 'Basketball', checked: false },
{ label: 'Handball', checked: true },
],
});
Reflejándose de la siguiente manera:
Con lo que debemos de aplicar esos estilos, para que los activos tengan fondo verde y los demás, fondo rojo.
useStyles$(`
button {
background: red; /* Esto ya sería "no-checked"*/
}
.checked {
background: green;
}
`);
Y quedará de la siguiente forma con este último cambio:
Ahora lo que nos queda es añadir la función para efectuar el cambio de estado dependiendo de si hacemos click en una opción u otra para cambiar de estado.
import { component$, $, useStyles$, useStore } from '@builder.io/qwik';
export default component$(() => {
useStyles$(`
...
`);
const myHobbiesList = useStore({
options: [
...
],
});
const optionsSelectChange = $((index: number) => {
myHobbiesList.options[index].checked =
!myHobbiesList.options[index].checked;
});
const mySelectHobbies = useComputed$(() =>
...
);
return (
<>
<div class="section bright">
<div class="container center">
{myHobbiesList.options.map((value, index) => (
<button
key={value.label}
class={value.checked ? 'checked' : 'no-checked'}
onClick$={() => optionsSelectChange(index)}
>
{value.label}
</button>
))}
<h4>Mis hobbies actuales</h4>
{JSON.stringify(mySelectHobbies.value)}
</div>
</div>
</>
);
});
Si hacemos en la opción Trail Running
, pasaremos de esto:
A lo siguiente:
(Os recomiendo que lo probéis con las otras opciones)
Conclusión
Y llegados a este punto, se podría decir que ya hemos visto todo lo necesario para entender mejor useComputed$()
Hemos visto un concepto nuevo como el uso de useComputed$()
aparte de haber repasado conceptos como el uso useSignal()
y useStore()
.
Llevamos aprendido muchísimo, os animo a que repaséis si hiciese falta.
Me gustaría que en los comentarios dejaseis resultados de cosas que vayáis practicando, ya que cuanta más variedad, todo el mundo nos beneficiamos de ello.
Todos los artículos publicados del curso los encontraréis en la siguiente lista que iré actualizando semanalmente y estableciendo el orden natural recomendado:
Presencia en redes sociales
Podéis encontrarme en las siguientes redes.