Comenzamos con un nuevo capítulo donde seguiremos aprendiendo más acerca de la gestión de estados.
En este caso vamos a aprender a hacer uso del hook useComputed$()
, función que nos va a facilitar mucho el trabajo cuando trabajamos con valores computados, permitiendo hacer tracking (observar cambios) por defecto y encima de eso, devolver un valor como resultado.
Es muy importante haber entendido bien el capítulo anterior, donde trabajamos con los hooks de estado useSignal()
como useStore()
para gestionar el estado de una aplicación.
Los puntos que vamos a trabajar en este capítulo son los siguientes:
Para empezar a trabajar con ello, tendremos que tener claros los conceptos de estas dos preguntas:
useComputed$()
?useComputed$()
es la forma preferida de crear valores computados que nos va a permitir 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.
useComputed$
En la siguiente referencia tenemos información sobre esta función, aunque con lo que veamos en este capítulo no va a ser necesario usarla.
Ejemplos que podrían realizarse, entre otros ejemplos que podremos ver en este capítulo:
Antes de empezar a trabajar nuestro proyecto, dejamos de lado el proyecto anterior llamado 08-state-management-i y creamos uno nuevo, siguiendo los pasos expuestos anteriormente. Como sugerencia os animo a que lo llaméis 09-state-management-ii.
Empezamos con lo más básico, desde lo que es la importación de la función hasta añadirlo en el 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';
export default component$(() => {
return (
<>
<h1>Hi 👋</h1>
<p>
Can't wait to see what you build with qwik!
<br />
Happy coding.
</p>
</>
);
});
Cuyo contenido se visualizará de la siguiente forma (o similar, puede cambiar con la versiones el apartado de estilos y estructura):
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 siempre duplicado por 2. Primero añadimos los dos valores:
// 1.- Añadimos los imports necesarios
import { component$, useComputed$, useSignal } from '@builder.io/qwik';
export default component$(() => {
// 2.- Iniciamos el valor del contador en 0
const valueCounter = useSignal(0);
// 3.- Definimos el valor que se obtiene con
// useComputed$, cada vez que cambie `valueCounter`
const doubleValueCounter = useComputed$(() => valueCounter.value * 2);
return (
<>
<h1>Use State Management - useComputed$ 👋</h1>
<p>
Valor con <code>useSignal</code>: {valueCounter.value}
</p>
<p>
Valor con <code>useComputed$</code>: {doubleValueCounter.value}
</p>
</>
);
});
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 (0 * 2 siempre 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.
Con este cambio hemos podido ver su comportamiento de una manera sencilla. 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;
color: white;
border-radius: 1rem;
font-size: 2rem;
}
`);
...
return (
<>
<h1>Use State Management - useComputed$ 👋</h1>
<p>
Valor con <code>useSignal</code>: {valueCounter.value}
</p>
<p>
Valor con <code>useComputed$</code>: {doubleValueCounter.value}
</p>
<button onClick$={() => valueCounter.value++}> + 1</button>
</>
);
});
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:
Tenemos ya las primeras nociones para pasar a otro ejemplo con el objetivo de reforzar lo aprendido en este punto.
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 (
<>
<h1>Use State Management - useComputed$ 👋</h1>
<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>
</>
);
});
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.
Teniendo estos aspectos claros, realizamos 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 elementos más complejos. Esto lo haremos mediante useStore()
que será muy útil para lo que corresponde a la práctica final de este capítulo.
Comenzamos con un nuevo apartado donde seguiremos trabajando con useComputed$()
, donde mantenemos el valor de la selección del index (indexSelect
) para ir seleccionando los nombres y apellidos que añadiremos ahora mediante el botón de +1
.
Ampliamos 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
concatenados.
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 con la concatenación de los dos valores.
// 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;
color: white;
border-radius: 1rem;
font-size: 2rem;
}
`);
// 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 (
<>
<h1>Use State Management - useComputed$ 👋</h1>
<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>
</>
);
});
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 elemento con el hook 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 necesarios (aunque no cercanos a casos reales), vamos pasando al siguiente nivel, donde nos vamos a centrar en una lista de opciones con selección de activo o no.
Dispondremos de varias opciones que ya se asemejará más a usos reales que nos podamos encontrar en muchos proyectos.
Tal y como hemos trabajado anteriormente con useSignal
y useStore
, tenemos la opción de pasar nuestro estado a través de diferentes componentes haciendo uso de los props
y también mediante useContext
(usando Context API
).
Pasamos esta información del componente principal a su hijos pudiendo pasar estos a sus propios hijos, comunicándose nivel a nivel.
Siguiendo con lo que estamos trabajando, creamos un nuevo componente con este código:
export const ChildComponent = component$((props: {data: Signal<string>}) => {
useStyles$(`
.child {
border: 2px dotted green;
}
`)
return <p class="child">{props.data.value}</p>
});
Donde tenemos un prop
cuya información contiene una propiedad data
que obtiene el valor computado que le vamos a pasar del elemento useComputed$
llamado nameLastnameText
.
El tipo de dato que le asignamos con useComputed$
debe de ser con el tipo Signal
ya que emite una señal como en useSignal
y le debemos de decir que tipo de dato lleva.
En este caso, al devolver un string, debemos de especificar el tipo como Signal<string>
.
Teniendo esto claro, debemos de pasar el prop
desde el componente principal de esta manera tan sencilla:
<ChildComponent data={nameLastnameText}/>
Cuyo resultado se implementará así:
import {
Signal,
component$,
useComputed$,
useSignal,
useStore,
useStyles$,
} from '@builder.io/qwik';
export const ChildComponent = component$((props: {data: Signal<string>}) => {
useStyles$(`
.child {
border: 2px dotted green;
}
`)
return <p class="child">Pasando con props: {props.data.value}</p>
});
export default component$(() => {
....
// esto se ejecuta cada vez indexSelect.value sufre cambios
const nameLastnameText = useComputed$(
() => `${infoDataStore.select.name} ${infoDataStore.select.lastname}`
);
return (
<>
<h1>Use State Management - useComputed$ 👋</h1>
....
<ChildComponent data={nameLastnameText}/>
</>
);
});
Y esto se reflejará de la siguiente forma en nuestra aplicación:
Esto también podremos hacerlo con useContext
tal y como hemos visto en el capítulo anterior.
Añadimos el valor con useContext
que nos permitirá trabajar con el valor useComputed$
en cualquier componente que obtengamos el valor mediante el Context API
.
Como hemos visto anteriormente, esto nos va a permitir, independientemente del nivel a que se encuentre el componente donde obtenemos la información respecto al componente donde se inicia este estado, pasar la información sin respetar la jerarquía a los descendientes directos (hijos) mediante uso de los props
.
Aplicamos tal como hemos implementado antes pasando el valor de useSignal
anteriormente mediante el uso del Signal
. Iniciamos con lo siguiente:
import {
Signal,
createContextId,
} from '@builder.io/qwik';
export const CONTEXT_ID = createContextId<Signal<string>>('nameLastnameText');
Modificamos el componente <ChildComponent />
a lo siguiente, quitando el valor props
con la propiedad data
para coger el valor con useContext
:
export const ChildComponent = component$(() => {
useStyles$(`
.child {
border: 2px dotted green;
}
`)
const nameLastnameText = useContext(CONTEXT_ID);
return <p class="child">Pasando con props: {nameLastnameText.value}</p>
});
Y cambiamos también en el componente por defecto, quitando en el apartado <ChildComponent />
el valor data
que estamos usando como props
y almacenamos el valor de useComputed$ dentro del contexto creado CONTEXT_ID
con lo siguiente:
useContextProvider(CONTEXT_ID, nameLastnameText);
Y el resultado de todo el código, será el siguiente:
import {
Signal,
component$,
createContextId,
useComputed$,
useContext,
useContextProvider,
useSignal,
useStore,
useStyles$,
} from '@builder.io/qwik';
export const CONTEXT_ID = createContextId<Signal<string>>('nameLastnameText');
export const ChildComponent = component$(() => {
...
const nameLastnameText = useContext(CONTEXT_ID)
return <p class="child">Pasando con props: {nameLastnameText.value}</p>
});
export default component$(() => {
...
const nameLastnameText = useComputed$(
() => `${infoDataStore.select.name} ${infoDataStore.select.lastname}`
);
useContextProvider(CONTEXT_ID, nameLastnameText);
return (
<>
<h1>Use State Management - useComputed$ 👋</h1>
...
<ChildComponent />
</>
);
});
El resultado es el mismo (en lo que se refiere a lo visual) en el proyecto:
Con esto, ya tenemos estudiadas al detalle todas las opciones con useComputed$
.
Vamos a pasar a un ejemplo real aplicando una pequeña práctica.
Vamos a almacenar una lista de filtros, con las propiedades de label
y checked
para que las opciones aparezcam 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 los valores activos
, tengan un fondo 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,
},
],
});
Aquí es interesante que se especifique que añadiremos al botón las clases checked
y no-checked
, dependiendo de si está activo o no.
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;
color: white;
border-radius: 1rem;
font-size: 2rem;
}
`);
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 (
<>
<h1>Use State Management - useComputed$ 👋</h1>
{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)}
</>
);
});
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 con checked = true
:
Ahora eliminamos todo el contenido que tenemos en la parte donde se añade el código JSX donde mostramos 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$(`
...
`);
const myHobbiesList = useStore({
options: [
...
],
});
const mySelectHobbies = useComputed$(() =>
...
);
return (
<>
<h1>Use State Management - useComputed$ 👋</h1>
{myHobbiesList.options.map((value, index) => (
<button
key={value.label}
class={value.checked ? 'checked' : 'no-checked'}
onClick$={() =>
console.log('Con esto cambiaremos ', index)
}
>
{value.label}
</button>
))}
<h4>Mis hobbies actuales</h4>
{JSON.stringify(mySelectHobbies.value)}
</>
);
});
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 los 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, .no-checked{
background: red;
color: white;
border-radius: 1rem;
font-size: 2rem;
}
.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 su estilo.
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 (
<>
<h1>Use State Management - useComputed$ 👋</h1>
{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)}
</>
);
});
Si hacemos en la opción Trail Running
, pasaremos de esto:
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/OeF0CC2cHF
Al finalizar este capítulo, deberíamos de ser capaces de:
useComputed$
y como funciona.useSignal
, useStore
y useContext
con las diferentes opciones.Y llegados a este punto, se podría decir que ya hemos visto todo lo necesario para entender mejor useComputed$()
tanto en lo teórico como lo práctico, para poder aplicarlo en cualquier proyecto que sea necesario su uso.
Hemos visto un concepto nuevo como el uso de useComputed$()
aparte de haber repasado conceptos como el uso useSignal()
y useStore()
, con lo que ya tenemos más herramientas para poder gestionar mejor el estado de nuestras aplicaciones.
No olvidéis en repasar y sobre todo en practicar, fundamental.