Knowledge Hub
Qwik is a cutting-edge front-end framework designed for building efficient, resumable web applications. It stands out by enabling lightning-fast application startup times on the client side. Qwik achieves speed by downloading and executing only the necessary code for user actions.
Unlike traditional server-side rendered (SSR) frameworks, Qwik operates without the need for a process known as "hydration." In SSR applications, the client-side boot-up involves restoring event listeners, the component tree, and the application state. This restoration process, called hydration, can be resource-intensive as it necessitates the download of all component code associated with the current page.
Qwik's distinct advantage lies in its speed and efficiency. By circumventing the hydration process, it eliminates the need for downloading excessive code, resulting in a responsive and performant client-side experience.
Let’s explore the process of building a quiz application with Qwik.
Before you begin, you should:
Initialize a new Qwik application using the Qwik CLI. The CLI generates a basic starter template. Open the terminal or command prompt and run the Qwik CLI by executing one of the following commands based on your preferred package manager, whether it’s NPM, Yarn, or PNPM.
npm create qwik@latest
pnpm create qwik@latest
yarn create qwik
bun create qwik@latest
Start the development server:
npm start
pnpm start
yarn start
bun start
In building the quiz application, we would dive into the most important Qwik concepts such as directory-based routing, and state management.
Routing in Qwik relies on QwikCity, Qwik’s meta-framework. To learn more about QwikCity, an in-depth dive is available on Bejamas discovery. QwikCity uses directory-based routing, similar to frameworks like Next.js, SvelteKit, SolidStart, or Remix. Files and directories within the src/routes
directory define the routing structure of the application. To get started:
Inside your Qwik project's src/routes
directory, create a new folder named quiz
. Inside the quiz
directory, create an index.tsx
file.
In the index.tsx
file, content is defined using the export default component$(...)
syntax.
import { component$ } from "@builder.io/qwik";
export default component$(() => {
return (
<div class="section bright">
<h1>Quiz Page</h1>
</div>
);
});
Components serve as the fundamental building blocks of a Qwik application. They are defined using the component$()
function and, at the very least, are expected to return a JSX element.
Navigate to http://localhost:5173/quiz/ to see the new page.
In the defined component function, define the JSX markup.
return (
<>
{store.user ? (
<div class={styles["container"]}>
<h1>Quiz Page</h1>
<div>
<h2>
Question: {store.activeQuestion + 1}
<span> /{questions.length}</span>
</h2>
<h2>Time Left: {store.timer} seconds</h2>
</div>
<div>
{!store.showResult ? (
<div class={styles["quiz-container"]}>
<h3>{questions[store.activeQuestion].question}</h3>
{answers.map((answer, idx) => (
<li
key={idx}
onClick$={() => onAnswerSelected(answer, idx)}
class={
store.selectedAnswerIndex === idx
? styles["li-selected"]
: styles["li-hover"]
}
>
<span>{answer}</span>
</li>
))}
{store.checked ? (
<button onClick$={nextQuestion} class={styles["btn"]}>
{store.activeQuestion === question.length - 1
? "Finish"
: "Next"}
</button>
) : (
<button
onClick$={nextQuestion}
disabled
class={styles["btn-disabled"]}
>
{" "}
{store.activeQuestion === question.length - 1
? "Finish"
: "Next"}
</button>
)}
</div>
) : (
<div class={styles["quiz-container"]}>
<h3>Results</h3>
<h3>Overall {(store.result.score / 25) * 100}%</h3>
<p>
Total Questions: <span>{questions.length}</span>
</p>
<p>
Total Score: <span>{store.result.score}</span>
</p>
<p>
Correct Answers: <span>{store.result.correctAnswers}</span>
</p>
<p>
Wrong Answers: <span>{store.result.wrongAnswers}</span>
</p>
<button onClick$={() => window.location.reload()}>
Restart
</button>
</div>
)}
</div>
</div>
) : (
<div>
<h1>Login</h1>
<label>
Username: <input bind:value={username} />
</label>
<label>
Password: <input type="password" bind:value={password} />
</label>
<button onClick$={login}>Login</button>
</div>
)}
</>
);
});
export const head: DocumentHead = {
title: "Quiz app home",
};
This code block renders the quiz questions, quiz results and its options. It conditionally renders the quiz results when `store.showResult` is `true`. Displays the overall score, the total number of questions, the total score, the number of correct answers, the number of wrong answers, and a button to restart the quiz.
Create a new file quiz.module.css
inside the quiz
directory and import into the index.tsx
file.
import styles from "./quiz.module.css";
Now you can paste the styles from the GitHub repository to this file.
The data represents a set of quiz questions and answers, structured in TypeScript interfaces. The data is essential for populating the quiz content and testing the functionality of the application.
In the src
directory, create a data.ts
file and paste the code into the data.ts
file.
interface Question {
id: number;
question: string;
answers: string[];
correctAnswer: string;
}
interface Quiz {
totalQuestions: number;
questions: Question[];
}
export const quiz: Quiz = {
totalQuestions: 10,
questions: [
{
id: 1,
question: "What is the capital of Japan?",
answers: ["Beijing", "Seoul", "Tokyo", "Bangkok"],
correctAnswer: "Tokyo",
},
{
id: 2,
question: "Which gas do plants absorb from the atmosphere?",
answers: ["Oxygen", "Carbon Dioxide", "Nitrogen", "Hydrogen"],
correctAnswer: "Carbon Dioxide",
},
],
};
In Qwik, there are two types of state, static or reactive. The static state encompasses any data that is serializable, encompassing a wide range of types, including strings, numbers, objects, and arrays. The reactive state, on the other hand, is created using useSignal()
or useStore()
.
In this application, the useStore()
function is employed, and it differs from useSignal()
in its initialization process. Specifically, useSignal()
accepts an initial value, which can be a simple value like 0
, while useStore()
expects an object as its initial value. To get started:
Import quiz
object from the data
file. Import the useStore()
function from the @builder.io/qwik
.
import { component$, useStore, $ } from "@builder.io/qwik";
import { quiz } from "~/data";
Define a QuizStore
interface, this defines the structure of the state that the useStore
function will manage.
interface QuizStore {
activeQuestion: number;
selectedAnswer: any;
checked: boolean;
selectedAnswerIndex: null;
showResult: boolean;
result: {
score: number;
correctAnswers: number;
wrongAnswers: number;
};
nextQuestion: boolean;
}
Inside the component function, useStore
is used to initialize the application’s state using the QuizStore
interface. The useStore
function accepts an initial state object as its argument, which populates the initial values of the state properties.
export default component$(() => {
const store = useStore<QuizStore>({
activeQuestion: 0,
selectedAnswer: "",
checked: false,
selectedAnswerIndex: null,
showResult: false,
result: {
score: 0,
correctAnswers: 0,
wrongAnswers: 0,
},
nextQuestion: false,
});
return <>// JSX</>;
});
export const head: DocumentHead = {
title: "Quiz app home",
};
The store object is initialized with properties defined in the QuizStore
interface, providing initial values for the quiz-related variables. These variables include the active question, selected answer, the selected answer's index, whether to show quiz results and an object to store the quiz result details such as score, correct answers, and wrong answers.
To make the Quiz application useful, it’s important to be able to handle user responses and check if the user has entered the correct answers.
Accessing quiz data:
const {questions} = quiz
Here, the questions
array is extracted from a quiz
object. The array contains a list of quiz questions, each with its question statement, answer options, and the correct answer.
Extracting current question data:
const {question, answers, correctAnswer} = questions[store.activeQuestion];
store.activeQuestion
represents the index of the currently active question.
Handling user selection:
const onAnswerSelected = $((anwser: any, idx: any) => {
store.checked = true;
store.selectedAnswerIndex = idx;
if (anwser === correctAnswer) {
store.selectedAnswer = true;
console.log("true");
} else {
store.selectedAnswer = false;
console.log("false");
}
});
This function is called when a user selects an answer option. It takes two parameters: answer
and idx
(the index of the selected answer in the answers
array). The selected answer
is then compared with the correctAnswer
for the current question.
In the above code implementation, you may have noticed the $
sign in front of the function declaration. Qwik divides the application into smaller units known as symbols
. These symbols are finer-grained than components, allowing a component to be decomposed into multiple symbols. The process of splitting is managed by the Qwik optimizer.
The presence of the $
suffix serves as a signal to both the optimizer and developers, indicating when the division takes place. When using Qwik, developers need to be aware that certain specific rules come into play whenever they encounter the $
symbol. It's important to note that not all JavaScript code that is valid is a valid Qwik Optimizer transformation. At the end of the tutorial, there is a further discussion of the Qwik Optimizer transformation.
This section demonstrates how to update the quiz state and move to the next question after a user selects an answer.
Define nextQuestion
function:
const nextQuestion = $(() => {
// update the quiz score
});
The nextQuestion
function is responsible for advancing to the next quiz question after the user has interacted with the current one.
Updating the quiz score and answer status:
// update the quiz score
if (store.selectedAnswer) {
store.result.score += 5;
store.result.correctAnswers += 1;
} else {
store.result.wrongAnswers += 1;
}
If the user's selected answer store.selectedAnswer
is correct, increment the quiz score by 5 points and update the count of correct answers in the result object. If the selected answer is incorrect, increment the count of wrong answers in the result object.
Navigating to the next question:
if (store.activeQuestion !== questions.length - 1) {
store.activeQuestion += 1;
} else {
store.activeQuestion = 0;
store.showResult = true;
}
This checks if the current question is not the last question in the quiz by comparing store.activeQuestion
to questions.length - 1
. If it's not the last question, we increment store.activeQuestion
to move to the next question.
Resetting state for the next question:
store.selectedAnswer=null;
store.checked=false;
This resets the store.selectedAnswer
to null to clear the user's selected answer for the next question. Then, reset store.checked
to false, indicating that the user has not yet interacted with the next question.
It’s important to incorporate a countdown timer into the quiz application. A timer is a valuable addition to the application as it provides users with a time limit for answering each question.
Update application state:
Add the following code In the QuizStore
interface and QuizStore
state instance:
interface QuizStore {
timer: number;
intervalId: any;
}
export default component$(() => {
const store = useStore<QuizStore>({
// rest of code
timer: 10,
intervalId: null
})
})
The intervalId
keeps track of the active timer, the timer variable tracks the number of available seconds.
Create startTimer function:
The startTimer
function starts a countdown timer if there isn’t an already active timer. The timer checks the remaining time and decrements by 1 second. Once time runs out, it proceeds to the next question.
const startTimer = $(() => {
if (!store.intervalId) {
console.log("timer started");
const intervalId = setInterval(() => {
if (store.timer > 0 && !store.showResult) {
store.timer -= 1;
} else if (store.timer === 0) {
nextQuestion();
}
}, 1000);
store.intervalId = intervalId;
}
});
Update nextQuestion function:
There is a need to trigger the startTimer
function and reset the state in store.timer after navigating to the next question.
const nextQuestion = $(() => {
// rest of code
store.checked = false;
store.timer = 10;
store.nextQuestion = true;
});
Ensuring timer resumability:
When creating the timer functionality, it is important to have it run on the browser after rendering. In that case, the useVisibleTask$()
function from Qwik is used. The useVisibleTask$()
function registers a hook that triggers when the component becomes visible within the viewport. The hook executes at least once when the component loads in the web browser. It provides almost the same functionality as useEffect in React.
In the dependency section, import useVisibleTask$
:
import { component$, useStore, $, useVisibleTask$} from "@builder.io/qwik";
Inside the component body, define the useVisibleTask$
:
export default component$(() => {
// rest of code
if (store.nextQuestion === true) {
startTimer();
}
useVisibleTask$(() => {
startTimer();
});
});
This hook triggers the startTimer
function when the component turns visible.
Only authenticated users should be able to access the quiz. Let's see how to create authentication in Qwik and the handling of server logic. The server$()
function is a tool in Qwik that enables the creation of functions that run on the server. This capability is useful for tasks involving database access or server-side operations.
server$() is a way to establish a Remote Procedure Call (RPC) mechanism between the client and server. When you create a function using server$(), it takes on a specific signature: ([AbortSignal, ] ...): Promise<T>
. Let’s explore how to integrate functions created with server$() into our application.
Create server authentication function:
In the quiz
directory, create a file called server.ts
and paste the below code.
import { server$ } from "@builder.io/qwik-city";
export const serverAuthenticate = server$(
(username: string, password: string) => {
// Perform authentication logic here (e.g., check credentials against a database)
if (username === "user" && password === "password") {
// Authentication successful, return user information
return { id: 1, username: "user" };
} else {
// Authentication failed, return null
return null;
}
},
);
This code is meant to run exclusively on the server.
Import serverAuthenticate
:
In the index.tsx
file in the quiz
directory, import the serverAuthenticate
function from the server
file.
import { serverAuthenticate } from "./server";
Update Application State:
Add the following code In the QuizStore
interface and QuizStore
state instance:
interface QuizStore {
// rest of code
user: any;
}
export default component$(() => {
const store = useStore<QuizStore>({
// rest of code
user: null,
});
});
Importing useSignal()
into application dependencies:
useSignal()
creates a form of state called a reactive signal. It takes an initial value and returns a reactive signal. The reactive signal returned consists of an object with a single property called .value
.
import { component$, useStore, $, useSignal,useVisibleTask$} from "@builder.io/qwik";
Defining username and password state:
In this application, useSignal
is used to create state variables for tracking the value of the username and password entered by the user. Inside the component body, add the following code.
export default component$(() => {
const username = useSignal("");
const password = useSignal("");
});
Create login functionality:
When a user attempts to log in, the login
function is triggered. Inside the login function, a server authenticated process is initiated by calling the serverAuthenticate
function. This function communicates with the server to verify the provided credentials.
const login = $(async () => {
try {
const authenticatedUser = await serverAuthenticate(
username.value,
password.value,
);
console.log("authenticatedUser", authenticatedUser);
if (authenticatedUser) {
store.user = authenticatedUser;
} else {
alert("Authentication failed. Please check your credentials.");
}
} catch (error) {
console.error("Authentication error:", error);
// Handle the error (e.g., show an error message)
alert("An error occurred during authentication.");
}
});
After creating and testing the login functionality, an issue has arisen. useVisibleTasks$()
triggers when the component mounts, which means our timer starts running before log in. To prevent this, let's update the application.
Introduction to useTask$:
useTask$()
is similar to useVisibleTask$()
but provides added functionality. Unlike useVisibleTask$()
which runs immediately after the component renders, useTask$()
is reactive. This means that a task is trackable and re-executes when the tracked state changes.
In the dependencies, import useTask$
from @builder.io/qwik
and import isServer
from @builder.io/qwik/build
.
import { component$, useStore, $, useSignal, useTask$} from "@builder.io/qwik";
import { isServer } from '@builder.io/qwik/build';
Create userAuthenticated
state:
A variable for tracking authenticated users needs to be set up. The useTask$
tracks this variable and runs the timer when the user is successfully logged in.
Using useSignal
, create a reactive signal for checking user authentication.
const userAuthenticated = useSignal(false);
Update login function to update userAuthenticated
value:
const login = $(async () => {
try {
// rest of code
if (authenticatedUser) {
store.user = authenticatedUser;
userAuthenticated.value = true;
} else {
// rest of code
}
} catch (error) {
// rest of code
}
});
The userAuthenticated.value
updates to true once the user is authenticated.
Switch from using useVisible$()
to useTask$
:
Comment out the useVisbleTask$
and replace it with the code block below.
// Start the timer using useVisibleTask$ only if the user is authenticated
// useVisibleTask$(() => {
// // userAuthenticated.value = true;
// startTimer();
// })
useTask$(({ track }) => {
track(() => userAuthenticated.value);
if (isServer) {
return;
}
userAuthenticated.value = true;
delay(1000).then(() => startTimer());
});
With this update, the startTimer
function runs once the user is authenticated.
Voila. If you made it this far, the quiz application is complete and it’s time for you to play with it. Navigate to http://localhost:5173/quiz/ in your browser to see the application.
Qwik makes it easy to deploy applications with its ready-to-use integrations. Qwik City provides middleware that acts as glue code for connecting to server rendering frameworks. It enables connection to Cloudflare, Netflify, Vercel, and Express. To activate the middleware for connecting with a server rendering framework, run the below command.
npm run qwik add
This provides a wide array of middleware adapters to choose from based on your preference.
This tutorial makes use of Netlify edge functions, so choose Netlify Edge from the provided options.
With edge functions our application is thus rendered at an edge location near our users. Before proceeding further, install the Netlify CLI.
To install the Netlify CLI on Mac, in your terminal window, run the following command.
sudo npm i -g netlify-cli
To build the application for production, use the build command. This command runs npm run build.server
and npm run build.client
. This builds our site for both ssr and static content.
Run the following command:
npm run build
To deploy the application for development, run the following command:
npm run deploy
This command creates a website draft URL. The draft URL provides the ability to see how the application runs when served before deploying to production. Run the website draft URL to see how it behaves when served. If there are no errors, run the production deployment.
To deploy to production, run the following command:
netlify deploy --build --prod
Thank you for reading this far. You can access the production version of the application at the following URL: https://qwikquizapp.netlify.app/quiz/.
To log in, use the following credentials:
Also, you can find the code on the Bejamas page on GitHub.
Qwik is a JavaScript framework that excels in delivering fast and efficient web applications. Through this tutorial, we've built a functional Quiz application that showcases Qwik's capabilities. This application can be extended to handle various scenarios, including computer-based tests, multiple-choice questions for educational institutions, and board certification exams.
This tutorial has provided valuable insights into Qwik and how it can be used to create high-performance web applications. Thank you for joining this Qwik journey, and I look forward to seeing the amazing applications you'll build with this framework.
Let's explore critical aspects that underpin Qwik's efficiency and power, such as serialization and the Qwik Optimizer. Serialization, in the context of Qwik, plays a pivotal role in enabling resumable web applications.
Serialization, at its core, involves converting data into a format to be stored or transmitted and later reconstructed. This process is often compared to JSON.stringify
, a common method for converting JavaScript objects into strings. However, JSON has its limitations, and this is where Qwik steps in to provide solutions.
JSON's limitations pose challenges for developers, especially when dealing with complex application states. Qwik addresses some of these limitations head-on by:
While Qwik goes a long way in addressing the limitations of JSON serialization, it's essential to note that there are some limitations it cannot solve:
To harness the full power of Qwik and its resumability capabilities, developers need to adopt a new mindset when writing web applications. It's imperative to consider the resumability of both the framework and the application itself. This means expressing components and entities in a way that can be serialized and resumed without the need for re-bootstrapping.
Developers must shift from a heap-centric approach to a DOM-centric one. This paradigm shift requires a change in behavior and a retooling of web development skills. Frameworks like Qwik play a crucial role in providing guidance and APIs to ease this transition, making it easier for developers to write applications with serialization and resumability in mind.
While server-side rendering is the most apparent benefit of resumability, there are other advantages to consider:
The Qwik Optimizer operates as a Vite plugin during bundling. Its primary purpose is to divide applications into small, lazy-loadable chunks. It identifies and moves expressions, typically functions, into new files, leaving behind references to the original locations. The dollar sign ($) informs the optimizer which functions to extract into separate files and which ones to leave untouched. The optimizer relies on the $ suffix to identify functions for transformation.
The Qwik optimizer uses $ as a signal to extract code, and developers must adhere to specific rules when working with it. These rules ensure that the extracted code remains valid and optimized.
1. Allowed Expressions
The first argument of any function ending with $ must meet specific restrictions:
2. Closures
3. Module-declared variables
Consider a scenario where you want to implement a scroll event handler lazily. The traditional approach requires the manual separation of code into different files and hardcoding chunk names. However, Qwik simplifies this process by using the $() marker function.
By wrapping a function in $(), developers signal the Optimizer to move the function to a new file for lazy loading. It allows for easier code organization and chunking without manual intervention.
The Qwik Optimizer excels at breaking down code into smaller chunks, enhancing the application's maintainability and performance. For instance, when a component is defined with component$(), the Optimizer extracts its methods into separate files, improving code separation and facilitating lazy loading.
In complex scenarios where extracted function closures capture variables, Qwik handles the situation gracefully. It stores these variables in the QRL (Qwik Resource Locator) and generates code that restores the lexical scope when needed. This powerful feature enables the extraction of more functions into lazy-loaded resources, contributing to more efficient code splitting.
Understanding serialization and the $
in Qwik is essential for harnessing the full potential of the framework. By addressing JSON's limitations and emphasizing resumability, Qwik empowers developers to build high-performance web applications that are not only efficient but also capable of delivering a superior user experience.
The dollar sign ($) in Qwik is not just a symbol. It's a gateway to optimizing your web applications. By understanding the rules and techniques associated with the dollar sign, developers can leverage Qwik's capabilities to create highly modular, efficient, and maintainable web applications.