Getting Started with Qwik
Everyone is talking about the Qwik framework. In this video, I'll show you how to get started with it and what makes it so special. We will develop this food guide web app and in doing so, I will explain all the core features and the advantages of Qwik in comparison to frameworks like Angular, Next.js, and so on.
Let's go. First of all, what exactly is Qwik? Qwik is a JavaScript framework that was launched on its 1.0 version in March 2023. The creator of Qwik is Miško Hevery, who used to work at Google and has also developed Angular. Behind Qwik is the company Builder.io, which offers a headless CMS. However, you can use Qwik completely independently of this.
Let's just get started right away. We create a Qwik project. The prerequisite for this is that you have Node.js installed on your machine. We execute the command npm create qwik@latest, and we then get a small wizard. We select "Empty App" for this tutorial. We want to install the dependencies, and I don't need a Git repository. And it asked me if I want to hear a joke. How do you know if there's an elephant under your bed? Your head hits the ceiling.
Now, we open a created project, for example with Visual Studio Code, and finally start it with npm start. And tada, we have successfully got our first Qwik application up and running. Before we continue implementing, however, we need to take a closer look at what makes Qwik so special. And to do this, we first need to understand what the other frameworks do.
The Problem with Traditional Frameworks & Hydration
How does a web framework such as React, Next, Nuxt, or Angular, for example, actually work? When a website is requested, the client receives an almost empty HTML document which essentially only contains script elements and links to CSS resources. These are then loaded, executed, the component tree is built, the DOM is manipulated, views rendered, and so on. You can then interact with the application. A classic single-page application. The problem with this approach is that the complete code associated with the current page, and in some cases even the complete application code, must be downloaded before the user can interact with the application. This means that the larger the application, the longer the initial loading time. And in the past, this has increasingly become a problem for many companies that have built large applications. This approach does not scale. The loading time increases proportionally to the complexity of the application. The user waits a few seconds and sees nothing.
To tackle this, the big frameworks then reacted. Angular, for example, has introduced server-side rendering. In this case, a Node backend executes the application and renders an HTML document that looks like the application should look. This is then sent to the user instead of the empty HTML document. This means that the user immediately sees a view and is happy for the moment. However, this is only a static page. No application is running at this point, but the HTML document also contains the references to the script resources. This means that the client then downloads all the code it needs in the background, executes the code, and creates a state, binds event listeners to HTML elements, and so on. This is called hydration. Then the application is ready to use.
You may have experienced this when you go on a page and click on a button or other elements and nothing happens. And when you click again a few seconds later, it works. This is probably such an approach of server-side rendering and your application was not yet hydrated. Overall, this solution is somehow better for the users. They quickly get a view that they can look at. The application loads in the background, hydration occurs, and then the user can interact with the application.
However, the whole approach also means that the application is basically executed twice: first on the server to render the initial views, and then exactly the same thing happens again on the user's computer to actually initialize the application. So somehow it is not an ideal solution either. In addition, the users see something immediately, but it can still take a few seconds before they can interact with the page.
Qwik's Solution: Resumability and Lazy Loading
Qwik does things differently. It does not use a hydration approach. With Qwik, we have also a Node server which renders the view in the backend and sends it to the user. However, this HTML document also contains serialized data, such as the application state, component boundaries, event listeners, and so on. In principle, everything that is needed to start an interaction immediately. This serialized data can also be seen in the page code. This is contained in comments and HTML attributes. Of course, Qwik also needs some JavaScript which it has to load. However, it does not eagerly download the complete code, but only a very small script. It also does not have to execute a complete code and create a state as this is already embedded in the HTML. It does not have to bind many individual event listeners, but sets up one global event listener. This means that interaction with the application can start immediately.
Qwik calls this resumability. The user can resume execution directly from the point where the server stopped. This means that the execution is paused, so to speak, shifted to the client, and continued there. Very little data needs to be downloaded, and very little needs to be executed. At some point, however, a Qwik application also needs more code from the backend, and this is then loaded lazily as required. I click on a button, and only then is the code that is to be executed during this interaction loaded. Qwik refers to this as streaming of JavaScript. Lazy loading can sometimes be difficult to implement. The nice thing about Qwik is that as a developer, you don't have to worry much about it. It simply works like this by default. Code is split automatically, lazy loading is performed automatically. With Qwik, we often only see a dollar sign as the suffix of a function. This tells me as a developer that the Qwik optimizer can split the code here into smaller chunks and these are then only delivered to the user when required.
Let's take a look at this with a simple example. In the application we have created, we add logging instructions to the main page to see where they're executed and when. First, we write a logging in our component that will only be executed in the backend. And there is also a button with an onClick event that also logs something. We can immediately see the 'Hello world' from the backend because the application is hot reloaded in the background. In the browser, if you look at it in the front end, we see nothing in the console at first. The backend logging—this code is only executed on the server and not again on the client. The application in the browser then just resumes. And as soon as we click on the button, the front end logging is executed, and the code for this logging is loaded lazily. We look in the network tab and refresh the page again. There are two initial JavaScript calls with the basic functionality of the framework. And only when the button is clicked is further JavaScript code downloaded. And here we see the logging instruction. This code is only sent to the client when the user needs it.
But now let's finally get on with our little food guide app.
Project Structure, Routing, and Layouts
So that we can concentrate on Qwik for this tutorial, we install Tailwind for the styling. Then we can simply use the corresponding CSS classes. We install Tailwind and initialize it to generate some config files. In one of these files, tailwind.config.js, we add an expression so that Tailwind recognizes our template files. Then we add three Tailwind directives to the global CSS file and then we are ready to use Tailwind in the project.
A brief overview of the folder structure in the project. There's of course a node_modules folder that contains all dependencies, such as Tailwind. There's also a public folder containing public files such as images, which are then available in the root of the app. The src folder contains Qwik components, routes, styles, etc. One component already exists, route-head, which contains HTML for the head element. There is an index route, the page we've already seen, and a layout for it. We will come to that later. Also worth mentioning is the root file, which contains the HTML basic structure and the RouterOutlet, which then displays the individual routes.
Routing in Qwik is file system based and therefore similar to Next.js. This means that you simply have to create a folder with the name for a route and store a template file in it. Let's create an 'About Us' page. We create a folder and an index.tsx. Then we define a component. In this, we simply return HTML, define a root element, and insert a heading and a paragraph with vertical padding in it, which Tailwind should now allow us to do, and just some dummy text. In addition, we can always export a document head element in the routes where we can specify the title, as well as meta elements, styles, etc. for this particular route. We simply define a title here and correct the import. By the way, this import here is not from Qwik, but from Qwik City. Qwik City is the name of Qwik's meta-framework, which is more comprehensive for a web framework, for example, in terms of routing. Qwik City is to Qwik what Next.js is to React or Nuxt.js is to Vue. We start our application again and then we can call up the 'About Us' page in the browser. Great, that works. Tailwind also seems to work. Overall, however, it still looks a little poor. So let's take a closer look at the layout. The route takes the layout of the layout file in the routes directory. This contains a Slot component into which the route component is then inserted. Otherwise, the layout does not yet contain any HTML. We change this and define a container with a maximum width which we align in the middle. This layout now applies to all routes. We could also define our own layouts for specific routes and use them in subfolders or with a specific naming convention. However, one layout is sufficient for us here. But let's add a little more styling in the global CSS, and we apply certain Tailwind sizes to all heading elements there. Finally, we have a look at our root file and give the body a gray background color. We look at that again, and it seems to work. Cool.
Next, we create a component. In our case, this will be the header bar. To do this, we create a header folder and a header.tsx file in it. We now simply return HTML, define a white header bar, and arrange elements in it using a flex box. There is a logo area. We name the app 'Qwik Food Guide' and we create a simple navigation. Each navigation element is given a Link—a Qwik link. This is not an actual anchor link; it is then intercepted by the framework so we have a real single-page application behavior. However, we can still simply define the paths to the root page and to our 'About Us' page. The component is now ready, but of course we also want to use it. We add the header bar to the layout. So we go to the layout file and add it with an import, and that already looks much better. Of course, this component is now only used in one place and it's not reused, which is the sense of a component, but the whole thing serves demonstration purposes here. Besides, encapsulated code is also a good thing.
State Management with Signals
Let's now take a look at the topic of state and how to keep track of global values or component-specific values. Qwik uses signals for reactive state. I won't go into the concept in depth here, but let's look at some simple examples. In our index view, we define a variable user and use a signal for this. We pass the string 'John' here, and we then output the value of the signal in the template with curly brackets and the property value. We can also reuse the onClick event to update the value of the variable there. We assign 'Max' to it. In the browser, we now see 'Hi John', and after clicking on the button, the value updates successfully and the view is adjusted correctly. This is what we want.
Instead of useSignal, you can also use useStore for complex objects. We then set an object with the name attribute here, and then simply read and write this attribute instead of value. And that also works. We now want to load the venues that should appear on our page via an API call.
Fetching and Displaying API Data
To do this, we first need an API. We use JSON Server to achieve this. This is a simple tool that lets us run a mock API. We install JSON Server and make sure to select version 0, as only unstable newer versions are currently available and the functionality will probably change a little. But now we can create a JSON file, db.json, and simply copy some data into it with the names, addresses, description texts, and images of some venues or restaurants. By the way, you can find the complete code of this project linked in the video description. We start the endpoint via json-server --watch db.json, and now a server is running on localhost. And if we now go to /venues, the individual restaurants are displayed. And you can also query a single restaurant via an ID that you append as a path parameter. So we have the endpoint.
But how do we fetch the data? In our index file, we first create an interface Venue with ID, name, address, image, and description. To load the data from an API asynchronously in our component, we use useResource. We create a venueList object which should then become a list of our Venue objects. And here we fetch the objects from localhost and then cast the JSON result to a list of venues. In the template, we output a result with the Resource component. This is a convenient way of defining the different states. We pass our list as a value. We define an HTML element that should be output while the data is being loaded. And when the data is there, we simply output the number of data records to see if it works. And it does. Cool.
In the next step, we want to display the venues in a nice way with the information and images we have. We create a container with a three-column grid, and for each venue, we create a div, pass a key (the ID of the venue), which we need here in the iterator. And the venue tiles are white and have rounded corners. They contain an image that we display as the background image. We set the URL for this, and below the image, we add elements for the name and the address. And it already looks great.
Implementing a Dynamic Search Filter
But let's make it a little bit more complex to see how Qwik works when we want to change the view dynamically. We implement a simple search. To do this, we create a searchTerm signal. Then we use the track function to perform the API fetch again when the value of searchTerm changes. We simply append the search term to the URL with the query parameter q. In the returned HTML, we create a div element and inside of it a heading on the left and an input for the search on the right. We use the onInput$ event listener to update the signal with the input element's value. We look at it in the browser, and the search field has now appeared on our site. And we can actually filter for these venues when we search for something, which doesn't have to be the name, but the address, for example. That was easy.
In this example, we can now take a closer look at how Qwik minimizes unnecessary requests and execution time. When we refresh the page—in other words, retrieve it from the server—the server will make the API call, retrieve the venues, and return the HTML to the client. This means we find the tiles of the venues in the HTML code of the server response. No additional data call is made from the browser to the backend. Only when we enter a search term in the client is a new data call made. This is because the track function checks whether the component's state has changed, and this leads to a re-execution of the API call with the updated search term. This means that nothing is unnecessarily executed twice on the server or the client. And I think it's really cool that ready-to-use HTML comes from the server straight away. And search engines and bots will also like it if they don't have to wait until the DOM has been manipulated to parse the page. The DOM is only manipulated if something needs to change. By the way, if you made it to this point and you like this video, you would help me a lot to make such tutorials if you give this video a like and subscribe to my channel. Thank you so much. But now let's move on with some fine-tuning.
Optimizing Search with a Debounce Task
When we look at our application, it looks quite good. However, when we type something in a search, we always see a slight flickering during rendering. This is because the application fires a backend call with every keystroke. This is unnecessary. We can always wait a little, so we introduce a debounce. To do that, let's take a quick look at the concept of tasks. To put it simply, we can use a task to execute asynchronous operations during the initialization of a component or during a status change. And this works like this: we first create a second signal, searchTermDebounced, and we now track this signal for our resource. We use useTask, which receives a tracker and a cleanup callback. And what we do now is to track whether the search term changes, and in this case, we simply set searchTermDebounced with a small time delay. However, if the value changes before the time has expired, we cancel the timeout, and we use the cleanup callback for this. And let's take a look at that. If I now type something really quickly, we only see one data request, not another request for each keystroke. That was really easy.
Server-Side Routes & Data Loading
Finally, a brief introduction to the routeLoader. We can work with this if we only want to fetch data on the server side and don't need to reload something in the browser, which we have done with our search. We create a sub-page for the individual venues for our app. In doing so, we can also see how we can work with path parameters. In a new venue folder, we create a folder with [id] in square brackets, and we can now call up the page with /venue/some-id, and we receive this ID as a route parameter. As usual, we create an index file in this route folder.
We now use the routeLoader. This code only runs on the server after navigation. And what we do here is very simple: we call our API and want to have a specific venue. And for this, we read the id parameter from the path. This way we will retrieve a single venue. As a signal, in the template, we read the signal value, and in the HTML code, we simply output the image again and the description text of the venue.
Now we link from our index page to the sub-page. In every venue tile, we add a <Link> element and pass the ID. If you look into our app, this works very well. We now have sub-pages for every single venue. And here we can see again that there is no dedicated data call made by the browser. The sub-page is rendered completely on the server. The client doesn't have to do anything else.
That's it for this tutorial. If you have any feedback for me, please write it in the video comments. See you next time.