Learn how to ship impactful customer journeys with Builder

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Platform
Developers
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Qwik

Introducing Qwik City Server Functions

February 15, 2023

Written By Manu Mtz.-Almeida

Today we are thrilled to show what the Qwik Core Team has been working on:

  • Data Loaders: reactive low latency data loading in the server.
  • Form Actions: run server code on user interaction
export const useGetServerTime = loader$(() => {
  // This code runs in the server
  return new Date();
});

export const useAddUser = action$(async (user) => {
  const userID = await db.users.add(user);
  return {
    success: true,
    userID,
  };
});

export default component$(() => {
  // Data can be consumed during SSR and SPA!!
  // Rendering data using signals
  // no need for async and await - Qwik handles it for you!
  const serverTime = useGetServerTime();
  const addUser = useAddUser();
  return (
    <div>
      <p>Server time: {serverTime.value.toISOString()}</p>
      <Form action={addUser}>
        <input name="name" />
        <button type="submit">Add user</button>
        {addUser.value?.success && <div>User added successfully</div>}
      </Form>
    </div>
  );
})

The Server vs. Browser Duality

One of the core ideas of Qwik is to run your app on the server and continue its execution on the browser when the user interacts. While Qwik was designed from the ground up to be a Server-Side Rendered (SSR) and Single-Page Application (SPA) technology, it doesn't distinguish between server and browser, ie, it treats all code equally, all code might run in the server or the browser.

That's where Qwik City enters the scene, a meta framework built on top of the resumability capabilities of Qwik to bring routing and data-fetching patterns. Qwik City is to Qwik, what NextJS is to React, Sveltekit is to Svelte or Solid Start is to Solid.

In this great tweet from Theo (CEO of ping.gg), he points out 3 big camps of solutions for the server/client communication problem:

  • Different teams/companies: GraphQL / REST
  • Same team: tRPC / GraphQL
  • Same team using typescript: tRPC / Qwik City

tRPC hits the sweet spot for most web apps! This is exactly the problem that Qwik City server functions solve.

In the context of Qwik City, the problem becomes a little bit more complicated than hitting an HTTP endpoint, reactivity, validation, streaming, avoiding request waterfalls, caching, and the list goes on. That’s why this is a problem that’s worth solving at the meta-framework level!

Qwik's Data Loader solves the issue of quickly fetching data to be displayed in the HTML. It helps to ensure that the data is available in a timely manner. This helps to improve the user experience and reduce loading times.

Qwik City goes a long way to execute all relevant loaders as soon as possible and in parallel to keep latency as low as possible, in addition, loaders are connected to the reactivity system of Qwik powered by signals, so new data will automatically and efficiently update all parts of the app that depend on it.

import { loader$ } from '@builder.io/qwik-city';

export const useGetServerTime = loader$(() => {
  return new Date().toIsoString();
});

export default component$(() => {
  const serverTime = useGetServerTime();
  return (
    <div>{serverTime}</div>
  )
});

Thanks to defer, loaders can return a promise and start streaming HTML before completion, meaning that we can render and send parts of the site before the loader has resolved. This way, loaders that might take a longer time to resolve will not block the whole site.

import { loader$ } from '@builder.io/qwik-city';

export const useGetProduct = loader$(({defer}) => {
  const promise = expensiveFetch();
  return defer(promise);
});

export default component$(() => {
  const productResource = useGetProduct();
  return (
    <Resource value={productResource}
      onResolved={(value) => (
        <article>
          <div>Product name: {value.name}</div>
          <img src={value.src} />
        </article>
      )}
    />
  );
});

Data loaders are equivalent to Next's getServerProps(), Remix loaders, SvelteKit’s load function and SolidStart’s createResource function. They are server-only routines that are executed as soon as possible to reduce latency and provide data to the later rendering process.

However, Qwik is able to take these concepts even further and loader$() comes with some advantages!

Server loaders are just exported functions that can be consumed, imported, and bring all the type information without any extra errors or manual, error-prone typing.

In other solutions, developers are required to manually specify the types, because the loaders and the components are connected through some internal magic that the type system does not understand, for example:

export const loader = () => {
  return {
    id: '42',
    name: 'Xmas pants',
    description: 'Ugly Xmas pants to surprise all your family'
  }
};

export default () => {
  const product = useLoaderData();
  // product: any
  return (
    <div>Product: {product.name}</div>
  )
}

With Qwik, the type information naturally flows because the loaders references are implicitly imported, allowing TS to do its job for you:

export const useGetProduct = loader$(() => {
  return {
    id: '42',
    name: 'Valentines Teddy-Bear',
    description: 'Handmade and Guaranteed For Life.'
  };
});

export default component$(() => {
  const product = useGetProduct();
  // Type of product: {id: string, name: string, description: string}
  return (
    <div>{product.value.name}</div>
  )
})

Loaders can be located anywhere and referenced and imported from any file, bringing with them their types and data efficiently.

// Notice we're importing a loader from a different file!
import { useAuthSessionLoader } from '../../layout.ts';

// and we can have multiple loaders.
export const useGetLoggedInUserMessage = loader$(() => { 
  return { 
    message:'Glad to have you back!'
  }
});
  
export default component$(() => {
  const signal = useAuthSessionLoader();
  const loggedInUserMessage = useGetLoggedInUserMessage();

  return (
    <div>
      { signal.value.isLogin
        ? loggedInUserMessage.value.message
        : 'You are NOT logged in'
      }
    </div>
  )
})

Loaders are internally implemented as middleware. This means that loaders get full access to the Request and Response, allowing them to take control of the request header, status, and HTTP body.

Even Qwiks streaming SSR is implemented as a middleware, so even a custom error middleware can capture a crash coming from a component.

export const useGetActiveUser = loader(async ({params, redirect}) => {
  const productID = params['id'];
  const product = await db.getProduct(id);
  if (!product) {
    throw redirect(301, '/all-products');
  }
  return user;
});

Form Actions solve the problem of executing code in the server when there is some user interaction such as: submitting a form, signing in, or adding an item to the cart.

import { action$ } from '@builder.io/qwik';

export const useAddUser = action$((user) => {
  const userID = db.users.add(user);
  return {
    success: true,
    userID,
  };
});

Actions can be invoked programmatically with JS from the browser, but they work better as a <form> action endpoint.

Thanks to good old forms, actions can be executed even if the JS is disabled.

import { action$, Form } from '@builder.io/qwik-city';
import { component$ } from '@builder.io/qwik';

export const useAddUser = action$((user) => {
  const userID = db.users.add(user);
  return {
    success: true,
    userID,
  };
});

export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" />
      <button>Add user</button>
      {action.value?.success && <div>User added successfully</div>}
    </Form>
  );
});

Form actions, such as RESTful APIs, receive data from external sources, such as a browser, so validating is an extremely good practice.

However, the developer usually neglects this diligence because the lack of validation is usually not a problem until YouAreUnderAttackt™️.

Qwik City Actions put validation and type safety together to enjoy the benefits and boost security.

By default, actions come with built-in support for Zod, a TypeScript-first schema validation with static type inference.

export const useAddUser = action$(
  (user) => {
    // `user` is typed { name: string }
    const userID = db.users.add(user);
    return {
      success: true,
      userID,
    };
  },
  zod$({
    name: z.string(),
    age: z.coerce.number(),
  })
);

Wanna see a live app using the new APIs? Give a try to Promptle a game built with Qwik and Form Actions!

You can also see the source code on Github.

We're thrilled to ship these features following all the iterative feedback, and we can't thank the community enough for all the help.

A lot of work has been put into this release as we are honing in v1 for Qwik and Qwik City.

If you have any feedback, we’d love to hear it!

You can find the core team in our discord server or you can tweet out to QwikDev.

Let’s make the web Qwik!

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
AI8 MIN
How to Build Reliable AI Tools
WRITTEN BY Manu Mtz.-Almeida
November 15, 2024
Web Design11 MIN
Design Smarter with Figma Auto Layout
WRITTEN BYApoorva
November 13, 2024
Web Development10 MIN
A Guide to Server-Side Rendering
WRITTEN BYLuke Stahl
November 12, 2024