La gestión del estado es una parte importante de cualquier aplicación. En Qwik, podemos diferenciar entre dos tipos de estado, reactivo y estático:
El estado estático es cualquier cosa que pueda ser serializada: una cadena, un número, un objeto, un array... cualquier cosa.
NOTA: Para el momento que estás leyendo este post ya tenemos el lanzamiento de QwikV1. Si deseas conocer que nos trae Qwik te dejo el siguiente blog
Un estado reactivo por otro lado se crea con useSignal()
o useStore()
.
Es importante notar que el estado en Qwik no es necesariamente un estado de componente local, sino más bien un estado de aplicación que puede ser instanciado por cualquier componente.
useSeñal()
Utiliza useSignal()
para crear una señal reactiva (una forma de estado). useSignal()
toma un valor inicial y devuelve una señal reactiva.
La señal reactiva devuelta por useSignal()
consiste en un objeto con una única propiedad .value
. Si cambia la propiedad value de la señal, cualquier componente que dependa de ella se actualizará automáticamente.
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<button onClick$={() => count.value++}>
Increment {count.value}
</button>
);
});
Este ejemplo de arriba muestra como useSignal()
puede ser usado en un componente contador para llevar la cuenta. La modificación de la propiedad count.value
hará que el componente se actualice automáticamente.
Por ejemplo, cuando la propiedad se cambia en el manejador de clic de botón como en el ejemplo anterior.
useAlmacenar()
Funciona de forma muy similar a useSignal()
, pero toma un objeto como valor inicial. Se puede pensar en un store como una señal de valores múltiples o un objeto hecho de varias señales.
Si deseas aprender acerca de Qwikv1 te dejo el siguiente video
Utiliza el hook useStore(initialStateObject)
para crear un objeto reactivo. Toma un objeto inicial (o una función de fábrica) y devuelve un objeto reactivo.
NOTA Para que la reactividad funcione como se espera, asegúrate de mantener una referencia al objeto reactivo y no sólo a sus propiedades. p.e. doing let { count } = useStore({ count: 0 })
y luego mutar count
no desencadenará actualizaciones de los componentes que dependen de la propiedad.
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const state = useStore({ count: 0 });
return (
<>
<button onClick$={() => state.count++}>Increment</button>
<div>Count: {state.count}</div>
</>
);
});
El ejemplo anterior muestra cómo se puede utilizar useStore()
en un componente contador para realizar un seguimiento del recuento.
Si tu quieres aprender Qwik recuerda que puedes ver el siguiente video
Valores recursivos
Por defecto, useStore()
sólo realiza un seguimiento de las propiedades de nivel superior de su almacén, lo que significa que para que se registre una actualización, tiene que actualizar los valores en la propiedad de nivel superior.
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const store = useStore({
nested: { fields: { are: 'not tracked' } },
});
return (
<>
<p>{store.nested.fields.are}</p>
<button onClick$={() => (store.nested.fields.are = 'tracked')}>
Clicking me does not work
</button>
<br />
<button onClick$={() => (store.nested = { fields: { are: 'tracked' } })}>
Click me works
</button>
</>
);
});
Para que las actualizaciones se registren con la estrategia de seguimiento predeterminada, tendríamos que actualizar el nivel superior nested
asi:
store.nested = { fields: { are: 'tracked' } };
Para que el primer ejemplo funcione, podemos pasar un segundo argumento a useStore()
, y decirle que utilice la recursión para rastrear todos los valores de nuestro almacén, sin importar su profundidad.
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const store = useStore(
{
nested: { fields: { are: 'not tracked' } },
},
{ deep: true }
);
return (
<>
<p>{store.nested.fields.are}</p>
<button onClick$={() => (store.nested.fields.are = 'tracked')}>
Clicking me works because store is deep watched
</button>
<br />
<button onClick$={() => (store.nested = { fields: { are: 'tracked' } })}>
Click me still works
</button>
</>
);
});
Ahora, el componente se actualizará como se esperaba. Esto también rastreará valores individuales dentro de matrices.
Estado calculado
El estado computado se deriva del estado existente.
En Qwik hay dos formas de crear valores computados, cada una con un caso de uso diferente (en orden de preferencia):
useComputed$(): useComputed$()
es la forma preferida de crear valores computados. Utilícelo cuando el valor calculado pueda derivarse de forma sincrónica únicamente a partir del estado fuente (estado actual de la aplicación).
Por ejemplo, la creación de una versión en minúsculas de una cadena o la combinación del nombre y los apellidos en un nombre completo.
2. useResource$():
useResource$()
se utiliza cuando el valor calculado es asíncrono o el estado procede de fuera de la aplicación.
Por ejemplo, obtener el tiempo actual (estado externo) basándose en una ubicación actual (estado interno de la aplicación).
La forma de pensar sobre lo anterior es que observan el estado existente, y cuando el estado cambia el valor calculado se vuelve a calcular. El resultado es un nuevo valor calculado que es una señal devuelta por useComputed$()
o useResource$()
.
Además de las dos formas de crear valores calculados descritas anteriormente, también existe una forma de nivel inferior (useTask$() y useVisibleTask$())
de modificar/crear estado como resultado de un cambio de entrada.
Esta forma no produce una nueva señal, sino que modifica el estado existente o produce un efecto secundario.
useComputed$()
Utiliza useComputed$
para crear un valor calculado que pueda derivarse de forma sincrónica del estado existente de la aplicación.
import { component$, useComputed$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const name = useSignal('Qwik');
const capitalizedName = useComputed$(() => name.value.toUpperCase());
return (
<>
<input type="text" bind:value={name} />
<div>Name: {name.value}</div>
<div>Capitalized name: {capitalizedName.value}</div>
</>
);
});
NOTA: Dado que useComputed$()
es síncrono no es necesario realizar un seguimiento explícito de las señales de entrada.
useResource$()
Utiliza useResource$()
para crear un valor calculado que se derive de forma asíncrona o que requiera un estado externo (como hablar con un punto final REST.) El gancho se llama cuando se monta el componente (y cuando cambian los valores rastreados.)
El gancho useResource$
está pensado para utilizarse con el componente . El componente es una forma conveniente de mostrar el estado del recurso, como cargado, resuelto o rechazado.
Nota: Lo importante que hay que entender sobre useResource$
es que se ejecuta en la renderización inicial del componente (al igual que useTask$
.)
A menudo es deseable comenzar a obtener los datos en el servidor como parte de la solicitud HTTP inicial antes de que se renderice el componente.
La obtención de datos como parte de SSR es una forma más común y preferida de cargar datos y se realiza a través de la API routeLoader$.
useResource$
es más una API de bajo nivel que es útil cuando se desea obtener datos en el navegador.
Al igual que todos los hooks use, debe invocarse dentro del contexto de component$()
, y se aplican todas las reglas de los hooks.
En muchos aspectos useResource$
es similar a useTask$
Las grandes diferencias son:
useResource$
permite devolver un "valor".
useResource$
no bloquea la renderización mientras se resuelve el recurso.
Véase routeLoader$
para la obtención temprana de datos como parte de la petición HTTP inicial.
import {
component$,
Resource,
useResource$,
useSignal,
} from '@builder.io/qwik';
export default component$(() => {
const prNumber = useSignal('3576');
const prTitle = useResource$(async ({ track }) => {
track(() => prNumber.value); // Requires explicit tracking of inputs
const response = await fetch(
`https://api.github.com/repos/BuilderIO/qwik/pulls/${prNumber.value}`
);
const data = await response.json();
return (data.title || data.message || 'Error') as string;
});
return (
<>
<input type="number" bind:value={prNumber} />
<h1>
PR#{prNumber}:
<Resource
value={prTitle}
onPending={() => <>Loading...</>}
onResolved={(title) => <>{title}</>}
/>
</h1>
</>
);
});
Aunque es posible utilizar useResource$()
sin el componente , se proporciona por comodidad. El componente muestra automáticamente un contenido alternativo mientras se calcula el recurso.
NOTA: Durante el SSR, el componente pausará la renderización hasta que se resuelva el recurso. De este modo, el SSR no se renderizará con el indicador de carga.
Ejemplo avanzado
Un ejemplo más completo de obtención de datos con AbortController
, seguimiento y limpieza. Este ejemplo obtendrá una lista de bromas basada en la consulta escrita por el usuario, reaccionando automáticamente a los cambios en la consulta, incluyendo abortar las solicitudes que están pendientes.
import {
component$,
useResource$,
Resource,
useSignal,
} from '@builder.io/qwik';
export default component$(() => {
const query = useSignal('busy');
const jokes = useResource$<{ value: string }[]>(
async ({ track, cleanup }) => {
track(() => query.value);
// A good practice is to use `AbortController` to abort the fetching of data if
// new request comes in. We create a new `AbortController` and register a `cleanup`
// function which is called when this function re-runs.
const controller = new AbortController();
cleanup(() => controller.abort());
if (query.value.length < 3) {
return [];
}
const url = new URL('https://api.chucknorris.io/jokes/search');
url.searchParams.set('query', query.value);
const resp = await fetch(url, { signal: controller.signal });
const json = (await resp.json()) as { result: { value: string }[] };
return json.result;
}
);
return (
<>
Query: <input bind:value={query} />
<button>search</button>
<Resource
value={jokes}
onPending={() => <>loading...</>}
onResolved={(jokes) => (
<ul>
{jokes.map((joke) => (
<li>{joke.value}</li>
))}
</ul>
)}
/>
</>
);
});
Como vemos en el ejemplo anterior, useResource$()
devuelve un objeto ResourceReturn
que funciona como una promesa reactiva, conteniendo los datos y el estado del recurso.
El estado resource.loading
puede ser uno de los siguientes:
false
- los datos aún no están disponibles.true
- los datos están disponibles. (Resuelto o rechazado).
La llamada de retorno pasada a useResource$()
se ejecuta justo después de que finalicen las llamadas de retorno de useTask$()
.
<Resource />
<Resource />
es un componente destinado a utilizarse con useResource$()
que muestra un contenido diferente en función de si el recurso está pendiente, resuelto o rechazado.
<Resource
value={weatherResource}
onPending={() => <div>Loading...</div>}
onRejected={() => <div>Failed to load weather</div>}
onResolved={(weather) => {
return <div>Temperature: {weather.temp}</div>;
}}
/>
Vale la pena señalar que no es necesario cuando se utiliza useResource$()
. Es sólo una forma conveniente de mostrar el estado del recurso.
Este ejemplo muestra cómo se utiliza useResource$
para realizar una llamada de obtención a la API de agify.io. Esto adivinará la edad de una persona basándose en el nombre escrito por el usuario, y se actualizará cada vez que el usuario escriba el nombre.
import {
component$,
useSignal,
useResource$,
Resource,
} from '@builder.io/qwik';
export default component$(() => {
const name = useSignal<string>();
const ageResource = useResource$<{
name: string;
age: number;
count: number;
}>(async ({ track, cleanup }) => {
track(() => name.value);
const abortController = new AbortController();
cleanup(() => abortController.abort('cleanup'));
const res = await fetch(`https://api.agify.io?name=${name.value}`, {
signal: abortController.signal,
});
return res.json();
});
return (
<div>
<div>Enter your name, and I'll guess your age!</div>
<input
onInput$={(e: Event) =>
(name.value = (e.target as HTMLInputElement).value)
}
/>
<Resource
value={ageResource}
onPending={() => <div>Loading...</div>}
onRejected={() => <div>Failed to person data</div>}
onResolved={(ageGuess) => {
return (
<div>
{name.value && (
<>
{ageGuess.name} {ageGuess.age} years
</>
)}
</div>
);
}}
/>
</div>
);
});
Pasar estado
Una de las buenas características de Qwik es que el estado se puede pasar a otros componentes. Al escribir en el almacén sólo se volverán a renderizar los componentes que sólo lean del almacén.
Hay dos maneras de pasar el estado a otros componentes:
- Pasar el estado al componente hijo explícitamente usando props.
- Pasar el estado implícitamente a través del contexto.
Usando props
La forma más sencilla de pasar el estado a otros componentes es pasarlo a través de props.
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const userData = useStore({ count: 0 });
return <Child userData={userData} />;
});
interface ChildProps {
userData: { count: number };
}
export const Child = component$<ChildProps>(({ userData }) => {
return (
<>
<button onClick$={() => userData.count++}>Increment</button>
<div>Count: {userData.count}</div>
</>
);
});
Uso del contexto
La API de contexto es una forma de pasar estado a los componentes sin tener que pasarlo a través de props (es decir: evita problemas de perforación de props). Automáticamente, todos los componentes descendientes en el árbol pueden acceder a una referencia al estado con acceso de lectura/escritura al mismo.
Consulta la API de contexto para obtener más información.
import {
component$,
createContextId,
useContext,
useContextProvider,
useStore,
} from '@builder.io/qwik';
// Declare a context ID
export const CTX = createContextId<{ count: number }>('stuff');
export default component$(() => {
const userData = useStore({ count: 0 });
// Provide the store to the context under the context ID
useContextProvider(CTX, userData);
return <Child />;
});
export const Child = component$(() => {
const userData = useContext(CTX);
return (
<>
<button onClick$={() => userData.count++}>Increment</button>
<div>Count: {userData.count}</div>
</>
);
});
noSerializar()
Qwik asegura que todo el estado de la aplicación es siempre serializable. Esto es importante para asegurar que las aplicaciones Qwik tengan una propiedad de reanudabilidad. A veces es necesario almacenar datos que no pueden ser serializados.
Por ejemplo una referencia a una librería de terceros como Monaco editor. En tal situación usa noSerialize()
para marcar el valor como no serializable.
Si un valor es marcado como no-serializable entonces ese valor no sobrevivirá a eventos de serialización tales como reanudar la aplicación en el cliente desde el SSR del servidor.
En tal situación, el valor se establecerá como indefinido y dependerá del desarrollador reinicializar el valor en el cliente.
import {
component$,
useStore,
noSerialize,
useVisibleTask$,
} from '@builder.io/qwik';
import Monaco from './monaco';
export default component$(() => {
const store = useStore<{ monacoInstance: Monaco | undefined }>({
// Don't initialize on server
monacoInstance: undefined,
});
useVisibleTask$(() => {
// Monaco is not serializable, so we can't serialize it as part of SSR
// We can however instantiate it on the client after the component is visible
setTimeout(() => (store.monacoInstance = noSerialize(new Monaco())), 1000);
});
return <div>{store.monacoInstance ? 'Monaco is loaded' : 'loading...'}</div>;
});
Recuerda que puedes aprender más acerca de Qwik en el siguiente video