• Labs icon Lab
  • Core Tech
Labs

Guided: Qwik City Foundations

Qwik City is a JavaScript meta-framework for Qwik designed to develop fast hybrid applications with a unified server-side and client-side rendering model for optimal performance. In this lab, you will build a simple helpdesk ticketing app to learn about components, routing, data loading, form handling, validation, state management, and actions. You’ll learn how Qwik efficiently ships JavaScript only when needed through its unique “resumable” architecture, which supports progressively enhanced apps that work with or without JavaScript enabled.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 45m
Published
Clock icon Dec 16, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    In this guided lab, you'll learn how to build a simple helpdesk ticketing application with Qwik using the Qwik City meta-framework.

    What to Expect

    You should be familiar with developing moderately complex web applications. This lab uses TypeScript, but where possible explicit typings have been minimized to be less distracting. Qwik uses JSX and a component model for rendering, which is similar to HTML. If you've already used web frameworks like React, Angular, Vue, Svelte, or others, this should feel familiar.

    info> Stuck on a task? Find solution files in the solutions/ folder, organized by step. Solution files look like {step.task}-{file path}, like 1.1-routes~index.tsx which corresponds to Step 1, Task 1 in the src/routes folder and index.tsx file. Copying and pasting the contents of that file to the equivalent file in the Filetree will make the check pass and you can examine it for details. Some tasks require you to update multiple files. The final outcome of the lab is in the final folder you can download.

    What is the Difference Between Qwik and Qwik City?

    Imagine building an app that renders 95% of the HTML on the server and then fills in the remaining slices of client-side interactivity on-demand only when the user actually needs it. That is what Qwik excels at.

    Qwik is a JavaScript library similar to React that supports a unified component-based rendering model in the browser and on the server. It's main feature is that it provides instantaneous "resumability" when transitioning from the server to the client. This means it is optimized for fast rendering and data sharing, even for complex applications. Qwik relies heavily on server-rendered HTML and as a result, is able to ship very little initial JavaScript and "streams" JavaScript chunks as the user browses the app.

    Hydration consists of several steps that take longer the bigger your application is but Qwik's 'resumability' is instantaneous no matter how big the app is

    There is no "hydration step

    If you are coming from React, Angular or Vue, these libraries "hydrate" the client side single-page application (SPA) from the server to bootstrap the application. This means there's often a lot of initial JavaScript to load modules and host the runtime. Qwik does not hydrate the app; it serializes the entire state in HTML on the server so it can be "resumed" on the client with about 1KB of JavaScript instantaneously.

    That means as your app gets bigger, performance doesn't suffer which makes Qwik a great fit for enterprise-scale applications. Refer to Think Qwik for more on the concepts of resumability and delayed execution.

    Qwik City is to Qwik as Next.js is to React. It is a meta framework that provides routing, data fetching, server rendering, caching, and other APIs for building full applications using Qwik components.

    Getting Started with Qwik City

    The lab's initial state was created using the Qwik City starter template:

    npm create qwik@latest
    

    The starter template comes with Qwik City fully configured and ready to run with TypeScript.

    Running Your App

    To get started with this lab, you should get the app running! As you can see in the Terminal output, Qwik City uses Vite as the development server.

    You can view the demo homepage in the Web Browser tab, or by visiting your lab's unique environment URL:

    {{localhost:3000}}

    You should see a very plain welcome page and no helpdesk in sight! It's time to fix that.

  2. Challenge

    Working with Pages

    What is a Page?

    The default file open in the lab is src/routes/index.tsx.

    This is a page in Qwik City. Pages are part of routing, but pages are not routes. Pages can export a Qwik component that is rendered to HTML according to the routing rules. They can also export HTTP request handlers which you'll see later in the lab.

    Routes are the folders within the routes directory, the top-level being the "index" route.

    How to Create Routes

    The routes folder is where you place all your application routes. Routes follow a directory-based structure where each folder is a URL segment.

    Here are the routes in the Helpdesk demo app:

    | File | URL | | -------- | -------- | | routes/index.jsx | / | | routes/new-ticket/index.jsx | /new-ticket | | routes/tickets/[id]/index.jsx | /tickets/{id} | | routes/api/tickets/index.jsx | /api/tickets |

    The tickets/[id] route uses a dynamic route segment, denoted by the square brackets. When a value is passed, like /tickets/2342, then 2342 is made available as the route parameter id to components and requests.

    Pages are defined in the index.tsx files. In addition to .ts and .tsx pages, Qwik City also supports .md, and .mdx (Markdown) files.

    Qwik Uses JSX Rendering

    Similar to React, Qwik components use JSX to render HTML to the page on both the server and client.

    The h1 and p tags should look familiar. These are JSX elements, clearly corresponding to HTML elements of the same tag.

    The <> tag may not look quite as familiar -- this is a "fragment" element which does not correspond to any HTML tag, but allows you to render a JSX tree within it. You can think of it like a ghost element that disappears on render. It's required because a JSX tree requires a parent node, but you may not always want HTML rendered.

    The component$ Function

    To create Qwik components, you must use the component$ function. The component$ function takes another function as its argument and must return JSX.

    Qwik City supports instant reloading of the dev server, so now would be a great time to test that out and modify the homepage. Notice how as you type, the lab auto-saves the file and the Web Browser tab "hot reloads" to show new code. This makes for a very fast feedback loop while developing.

    Using Router Link Components

    In a Qwik City application, this is a fast experience but it can be made even faster using the Link component:

    import { Link } from '@builder.io/qwik-city';
    
    export default component$(() => {
      return (
        <Link href="/another-page">Go to another page</Link>
      )
    });
    

    The Link component will act the same way as a plain <a> tag when JavaScript is disabled, but when JavaScript is enabled it will perform a client-side only (SPA) navigation. This means Qwik only sends the minimum required HTML from the server to the client to perform the route navigation resulting in a faster user experience.

    Hard vs. soft navigations

    Sometimes when talking about routing navigations, the terms "hard" and "soft" navigations are used. A hard navigation is when the browser performs a full page-request lifecycle and the server renders the page (also called a Multi-Page App or MPA navigation). A soft navigation is handled completely on the client-side but may fetch HTML from the server to reconcile with the current HTML on the page (also called a Single-Page App or SPA navigation).

    You can tell which is which by inspecting the Network tab in your browser developer tools. Document requests are hard navigations, while fetches or even web socket requests are soft navigations.

    Qwik City Link components perform soft (SPA) navigations when JavaScript is enabled and hard (MPA) navigations when JavaScript is disabled.

    Clicking the link from the homepage will still result in the browser changing pages, and the URL updating accordingly. However, if you inspect the **Network** tab in your browser developer tools, you would see a Fetch request being made by Qwik that returns serialized data from the server:

    (image)

    This is a client-side or SPA navigation in action.

    Creating a Page

    The /new-ticket route is currently empty and returning a 404 error.

    To create a page in Qwik City, you need to export a component$ as the default export in the route module file. With the Web Browser open to /new-ticket, you should now see a basic skeleton of the page with a heading.

    The last thing you need to create basic pages in Qwik City is metadata.

    Document Head Metadata

    In HTML pages, there is usually metadata stored in the <head> part of the document, including <title> and meta tags.

    In Qwik City, route files can export a head object that provides this static metadata. You will not be able to see the results of this task in the Web Browser tab in the lab, so you will need to open up the preview URL:

    {{localhost:5173}}

    The browser tab should now display the page titles for both pages as you navigate.

    Pages are the basic building block in Qwik City. But as you can probably tell, to build even a simple multi-page app, you'll want to share common HTML like headers and footers across pages as well as global styles for your application.

    That's what Layouts are for.

  3. Challenge

    Working with Layouts

    What is a Layout?

    Go ahead and open the src/routes/layout.tsx file in the lab editor.

    This is a Layout file. Layouts wrap the current and nested routes with HTML using a Qwik component.

    Slot-based Rendering

    You will notice that the layout component$ looks very similar to the page component with one important difference.

    It renders a <Slot /> component.

    A Slot in Qwik is a placeholder that components can be rendered into, from anywhere in the descendent tree. In React, this would be similar to the children prop, in Angular it's called Content Projection, and in web components, it is the same-named <slot /> component.

    Sharing HTML

    Router pages are rendered into a layout's Slot component. Any JSX that surrounds the slot will be shared across pages. Layouts can also nest under each other for each route segment allowing complex UIs to be developed. As you updated the layout, the Web Browser should have been refreshing to update the HTML for the page.

    Referencing Components

    When you have basic HTML like this, it can make sense to include it all in the layout. However, one pattern you may find helpful is to extract presentational components into separate files to make things easier to manage.

    In Qwik City, components that are not page or route components can be placed outside the routes directory and imported. The lab uses a ~ import alias to represent the src base directory:

    import SharedComponent from '~/components/shared';
    ``` Now that you've added the `Header` component, the **Web Browser** should persist the shared HTML between page navigations.
    
    The next step is to style the HTML so it looks a bit more like an application.
  4. Challenge

    Working with Styles

    Qwik City supports several flexible ways of importing and using CSS to style your application depending on your needs.

    Importing Global Styles

    To apply styles across the app, the src/root.tsx file can be used to import a CSS file:

    import './styles.css';
    

    The module import will transform and inject the styles into the Qwik City app, without needing to name the CSS import. Viewing the app in the Web Browser should now display something a bit more styled! Looking good.

    Not every style needs to be global, however. It's often best to scope styles to a specific component to make the CSS easier to maintain and less likely to conflict with other component styles across the app.

    Importing Component Styles

    While JSX supports providing inline styles via the style prop, it's not recommended.

    It is also not recommended to import unnamed CSS files using an import statement like the globals.css file because it will apply to the whole application when the component is rendered.

    Instead, you can use Qwik's useScopedStyles$ hook to scope a CSS file so it only applies to the current component:

    import { component$, useScopedStyles$ } from '@builder.io/qwik';
    
    export default component$(() => {
      useScopedStyles$(`
        span {
          background: red;
        }
      `);
    
      return (<div><span>My component</span></div>)
    });
    

    When rendered, the scoped styles will be automatically converted into a unique CSS class applied to the component root element (the <div> tag).

    You can provide styles within the template literal, including dynamic expressions. However, you can also import a CSS file and pass it to useScopedStyles$ but it must use the ?inline hint for Vite to inline the file.

    import { component$, useScopedStyles$ } from '@builder.io/qwik';
    
    import styles from './styles.css?inline';
    
    export default component$(() => {
      useScopedStyles$(styles);
    
      return (<div><span>My component</span></div>)
    });
    

    The result is the same, but you may choose one approach over another depending on your use case. The Helpdesk application is looking much more like a real app now.

    There are quite a few other ways to style your application, including using CSS modules, SCSS, LESS, and utility libraries like Tailwind.

  5. Challenge

    Working with Forms

    The first feature the Helpdesk should support is creating new tickets. Now that you have a page, why don't you work on adding a way to fill in new ticket data?

    For that, you'll need an HTML form.

    The Form Component

    Qwik City provides the Form component to make building with HTML forms easier and more straightforward. It takes care of transitioning between the client and server, allowing to pass data seamlessly across the server boundary whether JavaScript is enabled or disabled.

    import { Form } from "@builder.io/qwik-city";
    
    /* in component JSX */
    <Form>
      <div class="form-group">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name" />
      </div>
    
      <div class="form-group">
        <label for="body">Body:</label>
        <input type="text" id="body" name="body" />
      </div>
    
      <button type="submit">Submit</button>
    </Form>
    

    When JavaScript is disabled, an HTTP POST form request is sent and a full page lifecycle occurs. When JavaScript is enabled, a fetch request happens in the browser and the components re-render.

    info> The .form-group class is a helper style created for the application, it is not related to Qwik City.

    Form inputs each use have a name attribute which is projected into a JSON object. In the example above, the form data would be submitted as:

    {
      name: "value",
      body: "value"
    }
    

    info> This lab uses a simple form but Qwik City supports more advanced notation for arrays and nested objects for complex forms as well. The page now displays several form inputs with a button to submit -- but when you click it, nothing really happens! The page just refreshes.

    What's really happening is that Qwik City is successfully submitting the form data to the same route, but it's not being handled by anything so the page simply refreshes.

    Using Route Actions

    To take action on the submitted form data, Qwik City has the concept of Route Actions, provided by the routeAction$ API.

    An Action is a hook that can respond to a client-side event like a form submission and executes logic on the server-side, including side effects like creating data in a database. The result of an action is serialized and passed down to the client which can react to it using a Signal.

    Actions are tied to routes, which means you can declare them alongside your page component and they must be exported.

    import { component$ } from '@builder.io/qwik';
    import { routeAction$ } from '@builder.io/qwik-city';
    
    export const useHandleSubmit = routeAction$((data, requestEvent) => {
      console.log("Action executed with", data, requestEvent));
    });
    
    export default component$(() => {
      const submitAction = useHandleSubmit();
    
      return (
        <Form action={submitAction}>
        {/* existing code */}
        </Form>
      );
    });
    

    If the form is submitted, the console.log statement will only be shown in the server-side logs, and never executes on the client. The handler will accept the submitted form data as an object and is returning a success flag with a (fake) ticket ID.

    Now when you click the submit button, more is happening on the server, but nothing visibly changes in the browser.

    Is there a way to "react" to the form submission in the component?

    Adding Reactivity

    The useCreateTicket action returns something called a Signal in Qwik that can be subscribed to for reactivity (not to be confused with the uppercase React library).

    A Signal is similar to useState in React or signals in other frameworks: it is a proxied object that wraps a value and notifies subscribers when the value is updated.

    Signals can be created with the useSignal hook and it returns an object with a value property that is a "box" that holds whatever value the signal represents.

    import { useSignal, component$ } from '@builder.io/qwik';
    
    const Counter = component$(() => {
      const counter = useSignal(0);
    
      return (
        <div>
          <button type="button" onClick$={() => counter.value++}>Increment</button>
          Count: {counter.value}
        </div>
    

    This is a simple client-side counter using a Signal. When the button is clicked with the onClick$ Qwik event handler, the browser executes the counter.value++ on the client-side and the signal is updated, causing the component to re-render.

    Signals on the Client and Server

    Signals primarily enable client-side reactivity, so that you can perform logic in the browser that reacts to changes. Qwik Signals created and returned by the server-side have their server value serialized as the initial value when "resumed" on the client-side.

    The signal returned from the useCreateTicket action is created on the server and provides the return value of the action once it's been executed so that you can perform conditional rendering within the consuming component:

    export default component$(() => {
      const action = useCreateTicket();
    
      if (action.value?.success) {
        // Render a success message, if present
      }
    
      /* rest of code */
    });
    ``` This time, clicking the submit button results in the ticket ID being printed under the button.
    
    **Pop quiz time:** Is the JavaScript conditional logic being executed on the client or the server in response to the form submission?
    
    To answer this question, it's worth spending a moment to step back and understand what is happening here as it is core to understanding how Qwik works.
  6. Challenge

    Working with Actions

    How Resumability Works

    Consider this diagram representing the ticket form submission flow:

    Qwik form submission diagram, detailed below

    Walking through it:

    1. A request is initiated by the browser as an HTTP POST through a JavaScript fetch call
    2. Qwik server handles this POST request, passing the deserialized form data as a JS object and the request event to the action handler
    3. The action always executes on the server, performing some side effect like creating a ticket
    4. The action returns some value, in this case a ticket ID and success flag
    5. Not only does Qwik serialize the value returned from the action, it also executes the server-side render of the ticket form component and serializes the new state and HTML in response
    6. The client-side handles this response, updating the DOM in response and syncs the serialized state to the HTML

    Importantly, at no point is the conditional logic executed (or even served) to the client to show the success message! It is executed on the server and the resulting HTML is passed to the client because the rendering of the message has no dynamic client-side interactivity.

    How does Qwik ensure there's no dynamic client code needed?

    This the magic of the $ sign you've been seeing throughout the code. The $ sign in Qwik has special meaning---it represents a transition boundary between the server and client. In fact, you can signal to Qwik that code should be considered for delayed execution or resumability using the $(...) function yourself but this is not covered in the lab.

    Handling Side Effects

    Since actions execute only on the server, they allow you to perform side effects -- like creating tickets in a database.

    This lab does not have a database backend, but any kind of Node-compatible database backend works such as MongoDB, PostgreSQL, and others as in any other Node.js application.

    Instead, the lab uses a simple in-memory object store that only persists until the server is restarted.

    info> In-memory objects will not persist across hot reloads while in development mode, so ticket data is seeded with a single ticket to make the demo easier to use and navigate without adding tickets. For a production application, you would want to persist data to external storage. If you enter ticket data then submit it, the success message still shows and you should see a console message in the Terminal like this:

    Created ticket { summary: 'test', body: 'test', id: 2 }
    

    Try submitting multiple tickets to see the ID increment by one each time.

    If you try submitting blank values for the summary and body fields, the ticket will still be created. That isn't good!

    Is there a way to validate the data so the action isn't executed?

    Validating Actions

    Qwik City supports validating actions using either schema validation or a custom validator function.

    Using Zod for Validation

    Zod is a third-party library included with Qwik City that lets you define a validation schema for a JS object. The Zod schema can be passed as the second argument to the routeAction$ function:

    import { routeAction$, zod$, z } from '@builder.io/qwik-city';
    
    const useFormSubmit = routeAction$((data) => {
      return { success: true }
    },
    zod$({
        summary: z.string().min(2),
        body: z.string().min(2),
      })
    );
    

    Qwik City will validate the form data object against this schema.

    info> In the previous task you needed to pass data as any to skip type-checking, but with Zod you no longer need to as the ticket data object will be statically typed according to the schema. The validation schema you provided is static but zod$ accepts a callback for dynamically building a schema too. Qwik City also provides a validator$ function you can use to create custom validation based on the request event.

    Where does validation take place?

    Validation is part of the route action, so it is executed on the server. If you want to execute validation on the client, you would do so yourself using a client-side event handler such as onChange$ or onBlur$ on the form inputs.

    If you try to submit a blank form now, or field values with less than two characters, the console log message no longer is displayed meaning the createTicket is not called due to validation being enforced.

    However, there's no user feedback on what the problem might be.

    Handling Field Errors

    If validation fails, the action will not be executed and the action's value will contain field errors that can be used to display and handle validation messages. The action.value.fieldErrors contains a dictionary of field names to error messages:

    export default component$(() => {
      const submitAction = useFormSubmit();
    
      if (submitAction.value?.fieldErrors?.["name"]) {
        /* render an error message for `name` field */
      }
    
      return (<div />);
    });
    ``` This time, error messages are now displayed underneath each input if it fails validation.
    
    Besides validation errors, you can also manually call `requestEvent.fail()` to fail an action and cancel its execution, providing custom data to render in the component.
    
    Now that tickets are being validated and being persisted to (in-memory) storage, it would be great to **load them** and list them on the dashboard.
  7. Challenge

    Working with Loaders

    To load data from a database or other location to use within a route, Qwik City provides the routeLoader$ API.

    Creating a Route Loader

    You can export one or more route loaders from a route file, and they are used the same way as actions within the component through Signal-based hooks:

    import { routeLoader$ } from "@builder.io/qwik-city";
    
    export const useProducts = routeLoader$(async () => {
      const productsResult = await fetch('https://api.mycompany.com/products');
      const products = await productsResult.json();
    
      return products
    });
    
    export default component$(() => {
      const products = useProducts();
    
      return (<div>Products: {products.value.length}</div>)
    });
    

    Like route actions, route loaders are only ever executed on the server and the data is serialized and passed as a Signal to the component. If there is no client interactivity, the data is rendered on the server and sent with the HTML.

    To load tickets to display on the Dashboard page, you can implement a route loader to get the tickets from the "database" (in-memory store). In the Terminal, there should be log messages that print out the array of tickets, including the initial test ticket:

    Got tickets [
      {
        id: 1,
        summary: 'Seeded test ticket',
        body: '..."
      }
    ]
    

    Since the console message is being displayed in the Terminal and not within the browser developer console (go ahead and check!), this confirms the loader only executes on the server.

    Rendering Lists

    To render a list of items in Qwik and JSX, you use the JavaScript array map function and making sure to provide a key prop that uniquely identifies each item:

    export const List = component$(() => {
      const products = useProducts();
    
      return (
        <ul>
          {products.value.map(product => (
            <li key={product.id}>{product.title}</li>
          )}
        </ul>
      )
    });
    

    This renders an unordered HTML list where each list item prints the ticket name, keyed by its ID (which should be unique across all tickets). The dashboard will now render a list of tickets that are stored in-memory.

    Go ahead and try to create a new ticket. Once created, click the Dashboard link to go back and you should now see the newest ticket added to the list.

    Navigating to Dynamic Routes

    Recall that one of the available routes in the application is /tickets/{id}.

    The page is not yet implemented but it would be nice to set up some Qwik City Link components to navigate to the ticket details page.

    To link to a dynamic route, for the href prop of a Link component you can use template literals, passing the route parameter as an expression:

    <Link href={`/products/{product.id}/{product.slug}`}>View product page</Link>
    ``` Run through the demo and creating another ticket. This time, the success message shows a link to view the ticket details, making it easier to jump straight to the new ticket or to create a new one.
    
    info> **Tip:** If you want to clear the form after submission, you can set the `spaReset` prop on the `Form` component.
    
    The dashboard also now displays links to each of the ticket details page.
  8. Challenge

    Working with Route Parameters

    The ticket details page is empty and will need to load the ticket from the in-memory database.

    Using Route Params

    In a Qwik City component, you can access route parameters with the useLocation hook:

    import { useLocation } from '@builder.io/qwik-city';
    
    export default component$(() => {
      const { params } = useLocation();
    
      /* do something with `params` */
    
    });
    

    Like all component code in Qwik by default, the useLocation hook runs on the server.

    Why don't you use it to print out which ticket ID is being displayed? Clicking a link from the homepage to view a ticket will now display a page with the route's id parameter printed out.

    The next step is to actually load the ticket data by its ID by using a route loader.

    Loading Dynamic Data

    You've already learned how to load data for a route, but this time you'll need to read the [id] parameter from the route segment which is dynamic.

    In the routeLoader$ callback, the first argument is a requestEvent which contains a params dictionary containing any route parameters (as string values):

    import { routeLoader$ } from '@builder.io/qwik-city';
    
    export const useProduct = routeLoader$(async (requestEvent) => {
      const { id } = requestEvent.params;
      const productId = Number.parseInt(id, 10);
    
      /* fetch product by ID */
    });
    

    If you view the ticket detail page, it should now print the summary and body of the loaded ticket.

    The only problem is that if you pass an ID that doesn't exist, you'll run into an error!

    Failing a Request

    Since route loaders have access to the incoming HTTP request, you can use the requestEvent.fail() function to provide error data or change the HTTP status of the page:

    import { routeLoader$ } from '@builder.io/qwik-city';
    
    export const useProduct = routeLoader$(async (requestEvent) => {
      const { id } = requestEvent.params;
      const product = await getProduct(id);
    
      if (!product) {
        return requestEvent.fail(404, { errorMessage: "Could not find product" });
      }
    
      return product;
    });
    

    When failed, the route loader value will contain a failed flag. This allows the page handle errors gracefully and render specific error messages:

    export default component$(() => {
      const { params } = useLocation();
      const product = useProduct();
    
      if (product.value.failed) {
        return <p class="alert alert-info">{product.value.errorMessage}</p>;
      }
    
      return (
        {/* this is safe to access */}
        <div>{product.id}</div>
      );
    });
    
    How Qwik Improves Loader Failures with TypeScript

    Qwik is built with TypeScript and handles this scenario well using a FailResult type utility. For example, underneath the conditional, product.value is guaranteed to contain product properties and will not contain failed or errorMessage. This is why it is safe to access product.value.id when rendering the JSX without worrying about undefined values.

    If you attempt to pass a non-existent ID (or a string value), the page should now display a friendly error message and the document status will switch to a 404 instead of 200.

    Dynamic Document Head Metadata

    The other two pages in the app use static document head metadata, but this route is dynamic.

    You can export a head constant that is a function, which allows you to have dynamic metadata based on the incoming request:

    import {
      type DocumentHead,
      type DocumentHeadProps
    } from '@builder.io/qwik-city;
    
    export const head: DocumentHead = (props: DocumentHeadProps) => {
      return {
        title: "Viewing Product " + props.params.id
      }
    };
    

    The dynamic document head function accepts props of type DocumentHeadProps which includes request event properties, similar to loaders and actions.

    Can you spot the problem, though?

    While you can await the getTicket function in the document head function, it would result in a duplicate fetch request, effectively loading the data twice on every request.

  9. Challenge

    Working with Data

    Sharing Loader Data

    Qwik City provides a resolveValue function that is available on requestEvent objects (and document head props) which can be passed a loader instance. The function returns the resolved loader Promise value you can then reuse:

    import {
      type DocumentHead,
      routeLoader$
    } from '@builder.io/qwik-city;
    
    export useProduct = routeLoader$(/* ... */);
    
    export const head: DocumentHead = ({ 
      resolveValue, 
      params 
    }) => {
      const product = resolveValue(useProduct);
    
      return {
        title: `Viewing Product ${params.id} - ${product.title}`
      }
    };
    

    Loaders can be shared and reused like this across your application to avoid duplicating data requests, as long as they are exported from a route file like a layout or page.

    Sharing Across Routes

    In the Helpdesk header, the team asks if it's possible to show the number of active tickets next to the Dashboard link.

    Could you try reusing the useTickets loader from the index route? Depending on which page you were on when you worked on the previous task, either it worked or it's throwing the following error message:

    routeLoader$ "useTickets" was invoked in a route where it was not declared.

    If you're viewing the dashboard page, resolving the useTickets loader will work because the loader executes for that request. But on other pages, the loader isn't called.

    If you want to share data across multiple pages, loaders will need to be pulled up to the layout where they will execute for each route the layout is used on. By sharing the loader at the route level, it effectively removes the need for a page-level loader. And since Qwik executes loaders on the server, there is no additional overhead to running the loader for other requests.

    Sharing Data Deeper in the Component Tree

    This app is simple with a relatively shallow component tree. However, you may need to build an app where a component deep in the tree needs access to loader data. How would you share data then?

    Using Prop Drilling

    A simple but potentially inefficient pattern is to pass data from the top-level component down through the tree through props. However, this requires adding intermediary props to every child component until it reaches the one you want to consume the data in -- adding extra cognitive overhead and potentially leading to wasted re-rendering.

    Using a Qwik Context

    Prop drilling shared data is an anti-pattern in Qwik. As you saw throughout the lab, nearly all data can be shared to components using route loaders and actions.

    The best way to share data deeper in the component tree is by using a Qwik Context. A context can provide data to any of its descendant components in the tree through the useContext hook.

    Importantly, there is no additional performance overhead to doing this like you may have encountered in other frameworks like React. It is the most efficient way to share data in more complex applications and avoids prop drilling.

  10. Challenge

    Working with APIs

    Exposing Data Internally

    Go ahead and open the src/store/actions.ts file.

    Throughout the lab, ticket data has been fetched and stored to an in-memory object on the server.

    Route loaders and actions run on the server, but you can also explicitly mark functions as "server-only" functions using the server$ helper as seen in this file.

    This not only ensures the code runs on the server, it actually exposes an RPC-like interface so that you can call the functions from the client-side.

    Using Qwik Tasks

    So far you've only dealt with data on the server, but you can also invoke these server functions from the browser using Qwik Tasks.

    import { 
      useTask$, 
      useVisibleTask$, 
      useSignal,
      component$
    } from 'builder.io/qwik';
    import chart from '@third-party/charting';
    
    export const Chart = component$(({ data }) => {
      const chartImage = useSignal();
    	
      /* Runs once on server and anytime data changes on client */
      useTask$(({ track }) => {
        track(data);
    
        chartImage.value = chart.render(data);
      });
    
      /* Runs once on client */
      useVisibleTask$(() => {
        const timer = setTimeout(() => {
          console.log('Only on the client!');
        }, 2000);
    
        return () => clearTimeout(timer);
      });
    
      // ...
    });
    

    Tasks are similar to useEffect hooks in React but differ in important ways:

    • Tasks run least once and only run again if tracked state changes
    • useTask$ runs on the server but only runs on the client if tracked state changes
    • useVisibleTask$ runs only on the client
    • You can safely read any signals within the scope of the component
    What is "tracked state"?

    A task callback is passed an object with a track callback that is used to explicitly track component dependencies, like signals.

    If a task depends on a signal, when that signal value changes the task re-runs.

    If a useTask$ does not depend on tracked state, the code is not even sent to the browser.

    In many cases, useTask$ is ideal when side effects can run on the server but in the example, the setTimeout function only works in the browser so it must run within a useVisibleTask$.

    A task callback can return a cleanup function if it creates any resources (like a timer).

    Tasks allow you to invoke actions or server$ functions in response to changing component state, or the user's client-side environment.

    Marking Tickets as Read

    The team asks if you can implement a feature where a ticket is "marked read" after 2 seconds once the user views it. You can do this using a similar method the example above shows with the useVisibleTask$ function. If you view a ticket page, after two seconds the heading of the page will change from "(Unread)" to "(Read)".

    Pop quiz: Where does the task logic run: on the server or client?

    Reveal the answer!

    Both! Sorry, it's a trick question. Because you used useVisibleTask$, the setTimeout callback runs only on the client-side. However, the timeout callback invoked the getTickets server-side function using a fetch call. The isRead signal is updated on the client-side and the component re-renders with the changed heading. Once again, this demonstrates Qwik's unique resumability model in action.

    Exposing Data Externally

    If you do have a need to expose data from your application to the outside world for public consumption, you can also create more traditional API Endpoints in Qwik City.

    An endpoint is a route file that does not export a component but instead exports a request handler function:

    import type { RequestHandler } from "@builder.io/qwik-city";
    
    export const onGet: RequestHandler = async ({ json }) => {
      json(200, { hello: 'world' });
    };
    ``` You will need to click the API link below to open the tickets endpoint in your browser:
    
    {{localhost:3000}}/api/tickets
    
    Opening the link should display a formatted or unformatted list of tickets, depending on your browser configuration.
  11. Challenge

    Recap and What's Next

    Qwik is... Quick

    Qwik and the Qwik City meta-framework allows you to build complex hybrid server/client JavaScript applications using familiar concepts borrowed from popular frameworks like React and Angular.

    Instead of slowing down your application as it grows in complexity by shipping more and more JavaScript to the end-user, Qwik only ships the JavaScript actually required for interactivity, providing an instantaneous user experience when transitioning from the server to the client.

    What You Learned

    This lab covered the foundational concepts you need to build applications with Qwik:

    • How routing and navigation works (SPA vs. MPA)
    • How to create pages and layouts
    • Loading data using route loaders
    • Mutating data with route actions
    • How Qwik's resumability works
    • Adding reactivity with signals
    • Performing side effects using tasks
    • Validating and handling form errors
    • Creating API endpoints

    What's Next

    From here, you should feel more comfortable diving deeper into more intermediate to advanced features in Qwik City, such as:

    • Routing groups, catch-alls, and caching
    • Custom validators
    • Cookies, headers, and middleware
    • Unit and integration testing
    • Deploying to platforms
    • Custom $ resumable functions

    info> You can learn more about these topics on the Qwik website.

Kamran Ayub is a technologist specializing in full-stack web solutions. He's a huge fan of open source and of sharing what he knows.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.