Qwik — State Management

Claves para entender como funciona el control de estados y casos prácticos

Anartz Mugika Ledo🤗
16 min readFeb 28, 2023

La gestión de Estado (State) es una de las partes más importantes de cualquier aplicación, sea en Qwik o en tecnologías como Angular, React,…

Con esto estaremos controlando el flujo de la información en tiempo real dentro de nuestra aplicación de manera global, de manera aislada en un componente o mediante la comunicación de ese flujo de información entre diferentes componentes mediante el uso de props (concepto visto anteriormente en el artículos sobre componentes) u otras formas que iremos viendo en este artículo.

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.

Con lo que vayamos a trabajar en este artículo, vamos a conseguir el siguiente resultado:

Todos los artículos publicados del curso los encontraréis en la siguiente lista que iré actualizando semanalmente y estableciendo el orden natural recomendado:

Qwik paso a paso desde 0 al detalle

23 stories

Los requisitos a tener en cuenta son los mismos que en todos los artículos y para trabajar en este artículo crearemos un nuevo proyecto en una versión más actual de la que hemos estado trabajando hasta este mismo instante. Ya sabéis como se hace, a día de hoy (28/02/2023) la versión más actual es la 0.19.2

Esto es lo que vamos a ver en este artículo:

  • Introducción a los estados
  • useSignal
  • useStore.
  • Valores recursivos en los estados
  • Pasando los contenedores de estado a otros componentes.

Introducción a los estados

Ya sabemos que es un estado de manera global y para que usamos. En Qwik, nos vamos a encontrar con dos tipos de estado, reactivo y estático.

El estado estático es cualquier cosa que se pueda serializar: una cadena, un número, un objeto, una matriz… cualquier cosa. Esto sería asignar un valor y que se quede sin cambiar, simplemente visualizar y ya.

El estado reactivo, por otro lado, se va a crear con los hooks useSignal() o useStore().

Estos se encontrarán a la espera de posibles cambios, como podría ser un valor de un contador, la información de respuesta de una llamada a un endpoint de una API, la actualización de la hora,...

Es importante mencionar que el estado en Qwik no tiene que ser necesariamente el estado a nivel de componente local, sino que podría ser el estado de la aplicación instanciado por cualquier componente.

Una vez realizada la introducción, empezamos a trabajar con las diferentes formas de controlar el estado y empezaremos desde el hook useSignal().

useSignal()

Es un hook que crea una señal reactiva mediante const signal = useSignal(initialState), que obtiene un valor inicial (con initialState) y nos devuelve como resultado una señal reactiva dándonos como resultado un valor que usaremos en la aplicación.

Esto lo usaremos generalmente con valores simples sin mucha complejidad como valores primitivos como strings, enteros,…Para trabajar con elementos como objetos que muchas veces son de varios niveles (mediante nesting), vamos a usar el hook useStore(), que lo veremos más adelante en este artículo.

La señal reactiva devuelta por useSignal() consiste en un objeto con una sola propiedad (llamada signal.value). Si cambia la propiedad value del objeto, se actualizará cualquier componente que dependa de él.

Para reflejarlo dentro de un componente, debemos de realizar lo siguiente:

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

export default component$(() => {
const signal = useSignal(<initialState>); // <======
// <initialState> = valor inicial que se asigna
...
});

Por ejemplo, si inicio signalValue de la siguiente forma:

const signalValue = useSignal(19191);

En este momento el valor de signalValue.value será 19191. Si cambia la propiedad value del objeto, se actualizará cualquier componente que dependa de él, pudiendo visualizarlo haciendo simplemente referencia a signalValue.value

Aplicando los conceptos a un ejemplo muy sencillo

Este ejemplo se muestra cómo se puede usar useSignal() en un componente que mostrará el valor aleatorio siempre que pulsemos el botón con valores comprendidos entre 0 y 1.

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

export default component$(() => {
const randomValue= useSignal(Math.random()); // Inicia con un valor aleatorio
return (
<>
<button onClick$={() => randomValue.value = Math.random()}>Obtener aleatorio</button>
<p>Valor aleatorio actual: {randomValue.value}</p>
</>
);
});

Esto es lo que se consigue con el código que acabamos de implementar:

El simple hecho de acceder a la propiedad randomValue.value hará que el componente se actualice si cambia el valor de la señal por la reactividad que compone este hook.

En este caso particular se efectúa el cambia en el momento que ejecutamos la acción de click en el botón con la etiqueta Obtener aleatorio

Una vez visto esto, pasamos al siguiente elemento para poder trabajar con la gestión del estado y lo que vamos a usar es el hook useStore()

useStore()

Funciona de forma muy similar a useSignal(), pero toma un objeto como su valor inicial.

Para crearlo, lo iniciamos con const store = useStore(initialState) que es un hook que crea un objeto reactivo, tomando ese objeto inicial y devolviendo un objeto reactivo.

El objeto reactivo devuelto por useStore() es como cualquier otro objeto, pero es reactivo. Si cambia alguna propiedad del objeto, se actualizará cualquier componente que dependa de esa propiedad.

Ejemplo, con el típico contador

Este ejemplo muestra cómo se puede usar el hook useStore() en un componente de contador para realizar un seguimiento del recuento que irá incrementando con la acción de click del botón asignado a la acción del recuento.

Creamos un componente y añadimos el siguiente código:

export default component$(() => {
const counterState = useStore({ count: 0 });
return (
<>
<button onClick$={() => counterState.count++}>+ 1</button>
Count: {counterState.count}
</>
);
});

Esto es lo que tenemos al principio:

El simple hecho de acceder a la propiedad counterState.count hará que el componente se vaya actualizando a medida que hagamos click en +1. Se verá de la siguiente forma, donde ya se han hecho clicks en el botón +1 (exactamente 5).

Seguramente os lo habéis preguntado, ¿Y si no usamos el hook useStore y añadimos el valor del contador de la siguiente manera const counterState = {count: 0}? ¿Qué pasaría? ¿Actualizaría?

La respuesta es NO, ya que al no usar el hook estamos diciendo que no queremos esperar ningún cambio por lo que si asignamos el valor con 0 por ejemplo, se renderiza con 0 y listo.

Aunque estemos haciendo click en +1 una y otra vez, al no usar el hook useStore, no vamos a disponer de esa reactividad y no podremos recibir ninguna actualización en los componentes que estemos usando ese valor.

Os invito a que lo probéis y me contáis ;)

Valores recursivo dentro de los estados

Por defecto, useStore() solo va a estar observando los campos de nivel superior, lo que significa que para que se registren las actualizaciones, debe actualizar los valores en el campo de nivel superior.

Un ejemplo DONDE NO VA A FUNCIONAR la actualización del contenido es en hobbies debido a que vamos a actualizar una propiedad que no es de nivel superior. En cambio name y lastName si se actualizan debido a que están en el nivel superior:

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

export default component$(() => {
const store = useStore({
name: 'Anartz', // (1) - Top Level
lastName: 'Mugika Ledo', // (2) - Top Level
otherData: { hobbies: 'not tracked' }, // (3) - Nested Level (No top)
});
return (
<>
<ul>
<li>
{store.name} {store.lastName}
</li>
<li>Hobbies: {store.otherData.hobbies}</li>
</ul>
<button onClick$={() => (store.otherData.hobbies = 'tracked')}>
Click me to change hobbies
</button>&nbsp;
<button
onClick$={() => {
store.name = 'Anartz----';
store.lastName = 'Mugika_Ledo';
}}
>
Click me to change Principal
</button>
</>
);
});

Esto sería lo que conseguiríamos:

  • (1) Este será el valor que cambiará cuando hagamos click en el botón asociado a Click me to change Principal (2) que al ser dos datos que están en el nivel superior que serían name y lastName SI se actualizan.
  • En cambio, el apartado de los hobbies (3) NO se actualiza al hacer click en Click me to change hobbies (4) por no estar esta propiedad dentro del nivel superior ya que es una propiedad hija de otherData dentro del store

Al hacer click en las acciones, este es el resultado:

Para que las actualizaciones se registren en todos los niveles, ya que con la estrategia de seguimiento predeterminada solo observa los cambios del nivel superior, tendríamos que actualizar el apartado de nivel superior en el store de la siguiente manera, pasando de esto:

const store = useStore({
name: 'Anartz', // (1) - Top Level
lastName: 'Mugika Ledo', // (2) - Top Level
otherData: { hobbies: 'not tracked' }, // (3) - Nested Level (No top)
});

A lo siguiente:

const store = useStore({
name: 'Anartz', // (1) - Top Level
lastName: 'Mugika Ledo', // (2) - Top Level
otherData: { hobbies: 'not tracked' }, // (3) - Nested Level (No top)
},
{
{ recursive: true } // <=======================
});

A partir de la versión 0.100.0 de Qwik en vez de usar {recursive: true} debemos de usar {deep: true}. Si seguimos con la versión del tutorial, debemos de seguir con recursive. Si no tenéis en cuenta este detalle, no detectará los cambios en niveles más profundos. El artículo seguirá con {recursive: true}

Pasando ese segundo argumento a useStore() le decimos que use la recursividad para rastrear todos los valores en nuestro store, sin importar los niveles de profundidad que tenga, lo observará todo desde este momento. Así se reflejará aplicando los cambios en el componente donde estamos trabajando:

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

export default component$(() => {
const store = useStore(
{
name: 'Anartz', // Top Level
lastName: 'Mugika Ledo', // Top Level
otherData: { hobbies: 'not tracked' },
},
{ recursive: true } // <=======================
);
return (
<>
<ul>
<li>
{store.name} {store.lastName}
</li>
<li>Hobbies: {store.otherData.hobbies}</li>
</ul>
<button onClick$={() => (store.otherData.hobbies = 'tracked')}>
Click me to change hobbies
</button>
&nbsp;
<button
onClick$={() => {
store.name = 'Anartz----';
store.lastName = 'Mugika_Ledo';
}}
>
Click me to change Principal
</button>
</>
);
});

Ahora, el componente se actualizará como se esperaba, pasando el valor de Hobbies de not tracked a tracked

Estado inicial:

Hacemos click en los dos botones de acción:

Esto también va a rastrear posibles cambios individuales en los valores individuales dentro de los arrays.

Basándonos en lo que tenemos, pasamos de not tracked a añadir una lista de hobbies en formato string mediante un array:

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

export default component$(() => {
const store = useStore(
{
name: 'Anartz', // Top Level
lastName: 'Mugika Ledo', // Top Level
otherData: { hobbies: ['football', 'read', 'music'] }, // <===============
},
{ recursive: true }
);
return (
<>
<ul>
<li>
{store.name} {store.lastName}
</li>
<li>Hobbies: {store.otherData.hobbies.toString()}</li>
</ul>
<button onClick$={() => (store.otherData.hobbies = ['basket', 'photography', 'write'])}>
Click me to change hobbies
</button>
...
</>
);
});

Ahora tendremos lo siguiente:

Haciendo click, actualizará el listado completo:

También podemos reemplazar un elemento, por ejemplo el valor de football por correr:

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

export default component$(() => {
const store = useStore(
{
name: 'Anartz', // Top Level
lastName: 'Mugika Ledo', // Top Level
otherData: { hobbies: ['football', 'read', 'music'] }, // <===============
},
{ recursive: true }
);
return (
<>
...
<button
onClick$={() =>
(store.otherData.hobbies[0] = 'running')
}
>
Click me to change hobbies
</button>
...
</>
);
});

Al cargar:

Al actualizar football por running:

Incluso añadir nuevos, donde tendremos los 3 iniciales y si hacemos click, añadimos running, y se hará tantas veces como ejecutemos la acción de click:

...
onClick$={() =>
(store.otherData.hobbies.push('running'))
}
...

Esto será lo que tenemos:

Haciendo click UNA vez:

Haciendo click CUATRO veces:

Llegados a este punto, ya sabemos como podemos trabajar manejando el estado de la información de un store mediante el hook useStore tanto con los datos de nivel superior como con los datos que no son de nivel superior.

Ahora entra en juego la siguiente duda, ¿Es posible pasar estos contenedores de datos a otros componentes?

La respuesta es SI, tanto usando los props como mediante Context, que usaremos para un control de estado más global.

Vamos a implementarlo tanto con useSignal como useStore.

Pasando los contenedores de estado a otros componentes

Una de las características más interesantes y útiles de de Qwik es que el estado se puede pasar a otros componentes, y ambos pueden leerlo y escribirlo, lo que permite que los datos fluyan a través del árbol en todas las direcciones.

Hay dos formas de pasar el estado a otros componentes: con props o mediante el uso del contexto con Context API.

Usando props — useSignal

La forma más sencilla de pasar el estado a otros componentes es pasarlo como props. Esta es la forma en que lo haría en React, y también funciona en Qwik.

Podríamos perfectamente aplicar un ejemplo donde comenzamos asignando un valor inicial numérico mediante useSignal en un campo de texto y podemos ir cambiando de valor y a su vez se va mostrando los resultados de por ejemplo una tabla de multiplicación. Tenemos el componente llamado <Parent /> con el siguiente código.

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

export const PropsSignal = component$(() => {
return <Parent />;
});
export const Parent = component$(() => {
const multiplyValue = useSignal(1);
return (
<div style="border: 2px solid red; padding: 5px">
<input
value={multiplyValue.value}
onInput$={(ev) =>
(multiplyValue.value = +(ev.target as HTMLInputElement).value)
}
/>
<p>Valor a multiplicar: {multiplyValue.value} </p>
<Child multiplyValue={multiplyValue} />
</div>
);
});

Esto se vería de la siguiente forma de manera inicial:

Si cambiamos el valor del campo, por ejemplo a 45, el apartado “Valor a multiplicar” también cambiará con el valor que tenemos dentro del elemento input:

Ahora lo que nos queda es crear un componente hijo donde vamos a pasar el valor del elemento mediante los props que usaremos para la tabla de multiplicación y dentro de este mostraremos la tabla de multiplicación hasta 10.

Añadimos lo siguiente a lo actual, creando este componente:

export const Child = (props: any) => {
const { multiplyValue } = props;
return (
<div style="border: 2px solid green">
<p>Tabla de multiplicación: {multiplyValue.value}</p>
<ul>
{Array.from({ length: 10 }).map((_, index) => {
return (
<li>
{multiplyValue.value} * {index + 1} ={' '}
{multiplyValue.value * (index + 1)}
</li>
);
})}
</ul>
</div>
);
};

Y añadimos en la parte inferior la referencia del componente <Child /> junto con el valor que le vamos a pasar mediante los props de la siguiente forma:

<Child multiplyValue={multiplyValue} />

Y aplicándolo dentro del componente <Parent/>, lo dejamos de esta manera:

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

export const Child= (props: any) => {
...
};
export const Parent = component$(() => {
const multiplyValue = useSignal(1);
return (
<>
...
<p>Valor a multiplicar: { multiplyValue.value} </p>
<Child multiplyValue={multiplyValue} />
</>
);
});

Y este es el resultado que obtenemos:

Hay que fijarse en los bordes, lo que engloba a lo rojo es el componente <Parent/> y lo que está en borde verde es lo correspondiente al componente hijo llamado <Child /> que recibirá el valor de multiplicar y con ello calculará la tabla de multiplicaciones.

Ahora aplicamos mediante el ejemplo trabajado con el useStore.

Using props — useStore

Tal y como se ha realizado con el elemento contenedor creado con useSignal, vamos a pasar la información mediante el uso de props con useStore.

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

export const Parent = component$(() => {
const userData = useStore({
count: 0,
});
return (
<div style="border: 1px solid red; padding: 10px; margin: 5px">
Info in Parent (Counter {userData.count})
<hr/>
<Child userData={userData} />
</div>
);
});
export const Child = component$(({ userData }: any) => {
return (
<div style="border: 1px solid green;margin: 5px">
<button onClick$={() => userData.count++}>+1</button>
&nbsp;&nbsp;Count: {userData.count}
</div>
);
});

Esto sería el estado inicial:

Si realizamos tres click en el botón +1, el valor se tiene que actualizar tanto en el padre (<Parent />) como en el hijo (<Child/>), quedando de la siguiente forma:

Usando Context API — useSignal

El Context API es una forma de pasar el estado a los componentes sin tener que pasarlo a través de los props. Automáticamente, todos los componentes descendientes del árbol pueden acceder a una referencia al estado con acceso de lectura/escritura de manera muy sencilla.

Enlace al Context API para obtener más información.

Lo primero que tenemos que definir el identificador del contexto que usaremos como referencia:

import {
createContextId, // (A partir de la 0.18.1)
} from '@builder.io/qwik';

export const CONTEXT_ID= createContextId(<IDENTIFICADOR>);

En nuestro caso como va a ser para almacenar el contador:

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

export const CONTEXT_ID = createContextId('counter');

Para almacenar el valor usaremos useContextProvider teniendo en cuenta el identificador del contexto que hemos especificado con CONTEXT_ID y el valor a almacenar en el estado global de la aplicación, para poder recuperarlo en el componente que deseemos que se reflejará así:

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

// Asignamos el valor (state) al contexto (CONTEXT_ID)
useContextProvider(CONTEXT_ID, <VALOR_A_ALMACENAR>);

Aplicándolo en un ejemplo real donde vamos a tener 3 componentes, el primero el padre de todos <First /> y luego su hijo <Second /> y el tercero que será <Third />, nieto del < First /> e hijo del <Second />.

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

export const CONTEXT_ID = createContextId('counter');

export const First = component$(() => {
// Creamos el contenedor con el valor del contador
const counterSignal = useSignal(0);

// Asignamos el valor (state = counterSignal) al contexto (CONTEXT_ID)
useContextProvider(CONTEXT_ID, counterSignal);

....
});

Con esto, estaríamos almacenando el valor del estado para poder utilizarlo donde quisiéramos. ¿Cómo obtener el valor estemos donde estemos? Lo único que hay que hacer es usar el hook useContext y hacer referencia al CONTEXT_ID que hemos usado para almacenar esa información. En este caso lo haremos así:

const counter = useContext(CONTEXT_ID);

Lo aplicamos con dos componentes, el primero <First /> que será el padre y el segundo, <Second /> que será el hijo del primero:

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

export const CONTEXT_ID = createContextId('counter');

export const First = component$(() => {
// Creamos el contenedor con el valor del contador
const counterSignal = useSignal(0);
// Asignamos el valor (state = counterSignal) al contexto (CONTEXT_ID)
useContextProvider(CONTEXT_ID, counterSignal);

return (
<div style="border: 1px solid red; padding: 10px; margin: 5px">
Info in Parent (Counter {counterSignal.value})
<hr />
<Second />
</div>
);
});
export const Second = component$(() => {
const counter = useContext(CONTEXT_ID);
return (
<div style="border: 1px solid green;margin: 5px">
Second (Counter) : {counter.value}
<br />
<button onClick$={() => counter.value++}>+ 1</button>
<br />
</div>
);
});

El resultado se refleja de la siguiente forma:

Si hacemos dos veces click, debemos de observar que actualiza en los dos componentes el valor actual del contador cuyo resultado debe de ser dos:

Vamos a hacerlo ahora con un tercer componente que será hijo de <Second/ >:

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

export const CONTEXT_ID = createContextId('counter');
export const First = component$(() => {
...
});
export const Second = component$(() => {
const counter = useContext(CONTEXT_ID);
return (
<div style="border: 1px solid green;margin: 5px">
Second (Counter) : {counter.value}
<br />
<button onClick$={() => counter.value++}>+ 1</button>
<br />
<Third />
</div>
);
});
export const Third = component$(() => {
const counter = useContext(CONTEXT_ID);
return (
<div style="border: 5px solid orange;margin: 5px">
<button onClick$={() => counter.value++}>+1</button>
&nbsp;&nbsp;Count: {counter.value} (Third)
</div>
);
});

Y su resultado es el siguiente, que debe de actualizar en todos ellos el valor del contador cuando hacemos click en el botón de sumar:

Usando Context API — useStore

Habiendo visto todo lo anterior, ¿Seriáis capaces de hacer la adaptación a useStore desde useSignal?

Estoy seguro que si, y sin seguir leyendo, os invito a que intentéis hacerlo. ¿Cuál debería de ser el resultado?

Visualmente el mismo y deberá de funcionar de la misma forma, solo que usando el hook useStore.

El resultado os lo dejo aquí, para que hagáis la consulta después de haber completado la tarea.

Conclusión

Llegados a este punto hemos aprendido un nuevo concepto más, sobre la gestión del estado de la información dentro de los componentes.

Ya estamos cerca de hablar sobre el funcionamiento de los ciclos de vida y como consumir APIs, cuyos conceptos ya nos proporcionarán las herramientas necesarias para poder abordar proyectos de ámbito real.

Hasta ahora lo aprendido es mucho, 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.

Finalizamos el artículo y en el siguiente os voy a enseñar como consumir APIs REST y Graphql en un proyecto de Qwik.

Todos los artículos publicados del curso los encontraréis en la siguiente lista que iré actualizando semanalmente y estableciendo el orden natural recomendado:

Qwik paso a paso desde 0 al detalle

23 stories

Todo lo que hemos trabajado lo podéis encontrar en el siguiente repositorio con el resultado en código:

Si por casualidad no queréis trabajar descargando el proyecto, os dejo el resultado con lo trabajado en este Stackblitz (Qwik versión 0.16.2), para que juguéis y experimentéis:

Presencia en redes sociales

Podéis encontrarme en las siguientes redes.

--

--

Anartz Mugika Ledo🤗

[{#frontend:[#mobile:{#android, #kotlin, #ionic}}, {#web:{#angular, #qwik, #bootstrap}}],{#backend: [{#graphql, #nestjs,#express, #mongodb, #mysql}]}]