Netlify Developers

Compose Conference 2024 is coming!  Submit a talk

Adding resumability to Astro with Qwik

by Paul Scanlon

Astro avoids shipping any JavaScript to the client by default. However, there are occasions when client-side JavaScript is necessary. While Vanilla JS remains a viable option, frameworks are often preferred for their ability to address various challenges and streamline development processes, offering faster solutions for more complex use cases. Qwik takes an interesting approach to this, and boasts some interesting benefits. Let’s explore!

Quick

Qwik? Resumability?

Qwik is the first framework that has similar DX (Developer Experience) as React, Vue, or Svelte in how you author components, while delivering Live HTML that is instantly interactive. Qwik achieves this property by completely removing the need for hydration. Instead, Qwik applications immediately execute the event handlers on user interaction, without having to bootstrap all the app state. This technique is called Resumability. — Qwik

TL;DR

Within this guide, you’ll discover how to develop an Astro site leveraging Qwik’s Astro integration for client-side JavaScript functionality, as well as the process for deploying to Netlify. We’ll also explore some of the mechanics available and start building Qwik components.

An example site

You can see a preview of the deployed site and the repository for the code on the following links.

Just deploy and play

We’re going to get into some details here, but if you’d rather simply clone and deploy this example site right away, and then explore your newly cloned code, you can do that by clicking the button below.

Deploy to Netlify

Getting started with Astro

To get started with Astro run the following:

Terminal window
npm create astro@latest

If you prefer you can install Astro manually.

With an Astro site created you can commit the changes, push to your remote repository and deploy to Netlify.

Deploy with Netlify

Creating a new Astro site on Netlify is simple. Once you’ve logged in, you’ll be taken to https://app.netlify.com. Click “Add new site” to add a new site, follow the on-screen steps and all the default settings will be completed for you.

For more information about deploying, you can read our Get Started with Netlify guide.

Install the Qwik Astro integration

To install the integration run the following, then follow the instructions in your terminal.

Terminal window
npm install @qwikdev/astro

The repository README contains more detailed installation information.

After a successful install you should see the following changes have been made to your astro.config.mjs.

// astro.config.mjs
import { defineConfig } from "astro/config";
import qwikdev from "@qwikdev/astro";
export default defineConfig({
integrations: [qwikdev()],
});

If you’re using TypeScript with Astro be sure to also update your tsconfig.json with the following.

{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@builder.io/qwik"
}
}

Creating your first Qwik component

In the following code snippet you’ll create a button that, when clicked, will call a function that updates a counter value stored using Qwik’s useSignal Hook.

When signal values are updated, the component will re-render and the updated value will be displayed.

// src/components/counter-example.tsx
import { component$, useSignal, $ } from "@builder.io/qwik";
const CounterExample = component$(() => {
const counter = useSignal<number>(0);
const handleUpdate = $(() => {
counter.value++;
});
return (
<>
<h2>{counter.value}</h2>
<button onClick$={handleUpdate}>update counter</button>
</>
);
});
export default CounterExample;

Import and display

Qwik builds on top of Astro’s Zero JS, by default principle and then some. Thanks to resumability, the components are not executed unless resumed. Even with interactivity, the framework is also not executed until it needs to be. It is O(1) constant, … with zero effort on the developer. — Adding Qwik

As such, Qwik components don’t require Astro’s Client Directives. To see your new component import and return it in an Astro page.

// src/pages/counter.astro
---
import CounterExample from '../components/counter-example';
---
<html>
<body>
<h1>Getting Started with Netlify Astro and Qwik</h1>
<CounterExample />
</body>
</html>

Your first Qwik component explained

If you’re new to Qwik there are a few things to understand.

component$

All Qwik components are wrapped with component$. This allows Qwik’s optimizer to break them down and lazy-load them.

You can read more about components in the Qwik docs.

useSignal()

To update a signal, you apply changes directly to the .value. In the above example you are updating the value of the counter variable by using counter.value++.

You can read more about useSignal in the Qwik docs.

Event handlers $

Similar to the component declaration, event handlers are also wrapped with a variation of the $ syntax. Eg. $(...).

You can read more about reusing event handlers in the Qwik docs.

Event$

Qwik events are broadly the same as native JavaScript events, e.g onclick, but all will require the trailing $ syntax. E.g onClick$.

There are however some notable exceptions, for example onchange is onInput$.

You can read more about events in the Qwik docs.

Beyond the basics

The above example demonstrates a simple use of Qwik to define and manage client-side state, but Qwik can do so much more. Below you’ll find some examples of common requirements.

useStore()

useStore() is similar to useSignal() but is used when the state values are more complex than a single primitive. In the following code snippet you’ll create a component that contains an HTML select element that updates a store value, and a modified version of the counter, but this time instead of incrementing a number, you’ll push a new Rocket emoji to an array.

// src/components/emoji-example.tsx
import { component$, useStore, $ } from "@builder.io/qwik";
const EmojiExample = component$(() => {
const store = useStore<{ astronaut: string; rockets: string[] }>({
astronaut: "",
rockets: [],
});
const handleAstronaut = $((_: Event, currentTarget: HTMLSelectElement) => {
store.astronaut = currentTarget.value;
});
const handleRocket = $(() => {
store.rockets.push("🚀");
});
return (
<>
<p>
<span role="img" aria-label="Astronaut">
{store.astronaut}
</span>
</p>
<label>
astronaut:
<select onInput$={handleAstronaut}>
<option hidden>please select</option>
<option value="🧑‍🚀">🧑‍🚀 Astronaut</option>
<option value="👨‍🚀">👨‍🚀 Man Astronaut</option>
<option value="👩‍🚀">👩‍🚀 Woman Astronaut</option>
</select>
</label>
<hr />
<button onClick$={handleRocket}>add rocket</button>
<ol>
{store.rockets.map((data) => {
return (
<li>
<span role="img" aria-label="Rocket">
{data}
</span>
</li>
);
})}
</ol>
</>
);
});
export default EmojiExample;

Unlike useSignal, with useStore you update the value directly, rather than accessing the value using .value.

In this example the handleRocket function uses .push() to add a new Rocket emoji to the store value named rockets, which is typed as an array of strings.

The handleAstronaut function sets the store value named astronaut to equal that of the currentTarget.value.

You can read more about useStore in the Qwik docs.

A note on currentTarget

Qwik requires the use of an additional argument to access the currentTarget.value. Since event handling is asynchronous. The more common practice of using event.target.value can’t be used with Qwik.

useVisibleTask()

useVisibleTask() is one of Qwik’s built-in methods and is only invoked when a component is within the viewport. If a component is not within the viewport Qwik won’t attempt to perform any actions contained within the function body. This can be helpful for client-side fetch requests where you don’t want to fetch data for UI elements the user can’t see.

// src/components/client-fetch-example.tsx
import { component$, useVisibleTask$, useStore } from "@builder.io/qwik";
const ClientFetchExample = component$(() => {
const store = useStore<{ full_name: string; title: string; html_url: string }>({
full_name: null,
title: null,
html_url: null,
});
useVisibleTask$(async () => {
try {
const response = await fetch("https://api.github.com/repos/BuilderIO/qwik/pulls/1", {
method: "GET",
});
if (!response.ok) {
throw new Error();
}
const json = await response.json();
const {
html_url,
title,
head: {
repo: { full_name },
},
} = json;
store.html_url = html_url;
store.title = title;
store.full_name = full_name;
} catch (error) {
console.error(error);
}
});
return (
<div>
{store.html_url ? (
<div>
<h2>{store.full_name}</h2>
<p>{store.full_name}</p>
<a href={store.html_url}>{store.html_url}</a>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
});
export default ClientFetchExample;

In this example useVisibleTask is used to make a fetch request to the GitHub API and retrieve some data. If the request is successful the store values are updated with data from the response.

In the return statement, a loading message is displayed if the value of store.html_url is null.

You can read more about useVisibleTask in the Qwik docs.

Server-side rendering

To enable server-side rendering in Astro you can use the Astro Netlify Adapter. There is also an option to use Netlify Edge Functions which you can see in the docs here: Running Astro middleware on Netlify Edge Functions.

After a successful installation you will need to make following changes to your astro.config.mjs.

// astro.config.mjs
import { defineConfig } from "astro/config";
import qwikdev from "@qwikdev/astro";
import netlify from "@astrojs/netlify";
export default defineConfig({
integrations: [qwikdev()],
output: "server",
adapter: netlify(),
});

useTask()

With the Netlify adapter installed you can now use Qwik’s useTask to make server-side requests. However, Astro can also be used to make server-side data requests.

The below code is broadly the same as the useVisibleTask example, but the loading message has been removed since this data is requested and rendered on the server.

// src/components/server-fetch-example.tsx
import { component$, useVisibleTask$, useStore } from "@builder.io/qwik";
import { component$, useTask$, useStore } from "@builder.io/qwik";
const ClientFetchExample = component$(() => {
const ServerFetchExample = component$(() => {
const store = useStore<{full_name: string; title: string; html_url: string;}>({full_name: null, title: null, html_url: null});
useVisibleTask $(async () => {
useTask$(async () => {
try {
const response = await fetch('https://api.github.com/repos/BuilderIO/qwik/pulls/1', {
method: 'GET',
});
if (!response.ok) {
throw new Error();
}
const json = await response.json();
const {
html_url,
title,
head: {
repo: { full_name },
},
} = json;
store.html_url = html_url;
store.title = title;
store.full_name = full_name;
} catch (error) {
console.error(error);
}
});
return (
<div>
{store.html_url ? (
<div>
<h2>{store.full_name}</h2>
<p>{store.full_name}</p>
<a href={store.html_url}>{store.html_url}</a>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
});
export default ClientFetchExample;
export default ServerFetchExample;

You did it!

These are just a few examples of what you can achieve using Qwik. If you’re looking to continue your journey, why not explore the examples of state and tasks in the Qwik docs.