Getting Started Qwikly

Qwik is a new kind of framework that is resumable (no eager JS execution and no hydration), built for the edge and familiar to the React developers.

If you want to play with it right away, try our in-browser playgrounds:

Prerequisites

If you want to get started with Qwik locally, you will need the following:

Create an app using the CLI

The first step is to create a Qwik application with our CLI. The CLI will create a blank starter so that you can quickly familiarize yourself with it. Qwik supports NPM, yarn and pnpm.

Run the Qwik CLI in your shell. Choose the package manager you prefer and run one of the following commands:

npm create qwik@latest
pnpm create qwik@latest
yarn create qwik

The CLI will guide you through an interactive menu to set the project-name, select one of the starters and asks if you want to install the dependencies. To find out more about the files generated by reading up on the project structure.

Start the development server

npm start
pnpm start
yarn start

Qwik Hello World

To get you familiar with Qwik we have created a very simple "Hello World" application tutorial that touches on the most important concepts of Qwik. For each part we touch on we will link you to the relevant documentation where you can find out more about the given concept.

We will use https://icanhazdadjoke.com as our API to get a random joke. We will create a simple application that will display a random joke and a button to get a new joke.

Create A Route

Everything starts by serving a page at a particular route. So let's build a simple app that serves a random dad joke application on /joke/ route. Qwikcity (Qwik's meta-framework) uses directory-based routing. To get started:

  1. Create a new index.tsx file in the src/routes/joke/ directory in your project. (You will need to create the joke directory first.)
  2. Each route's index.tsx file must have an export default component$(...) so that Qwikcity knows what content to serve. Paste the following content to src/routes/joke/index.tsx:
import { component$ } from "@builder.io/qwik";

export default component$(() => {
  return <div class="section bright">A Joke!</div>;
});
  1. Navigate to http://127.0.0.1:5173/joke/ to see your new page working.

NOTE:

  • Your joke route default component is surrounded by an existing layout. See Layout for more details on what layouts are and how to work with them.
  • For more details about how to author components see the Component API section.

Loading Data

A common thing a page does is load data to display to the user. This is performed using route loaders.

  1. Open src/routes/joke/index.tsx and add this code:
import { routeLoader$ } from "@builder.io/qwik-city";

const useDadJoke = routeLoader$(async () => {
  const response = await fetch("https://icanhazdadjoke.com/", {
    headers: { Accept: "application/json" },
  });
  return (await response.json()) as {
    id: string;
    status: number;
    joke: string;
  };
});
  1. Then add the useDadJoke() hook to the default component and use the result in the JSX:
export default component$(() => {
  const dadJokeSignal = useDadJoke();
  return (
    <div class="section bright">
      <div>{dadJokeSignal.value.joke}</div>
    </div>
  );
});
  1. Navigate to http://127.0.0.1:5173/ to see the application running.

What does the above code do?

  • The function passed to routeLoader$ is invoked on the server eagerly before any component is rendered and is responsible for loading data.
  • The routeLoader$ returns a use-hook (useDadJoke) that can be used in the component to retrieve the server data.

NOTE:

  • The routeLoader$ is invoked eagerly on the server before any component is rendered, even if its use-hook is not invoked in any component.
  • The routeLoader$ return type is inferred in the component without the need for any additional type information.
CHECKPOINT: src/routes/joke/index.tsx:
import { component$ } from "@builder.io/qwik";
import { routeLoader$ } from "@builder.io/qwik-city";

const useDadJoke = routeLoader$(async () => {
  const response = await fetch("https://icanhazdadjoke.com/", {
    headers: { Accept: "application/json" },
  });
  return (await response.json()) as {
    id: string;
    status: number;
    joke: string;
  };
});

export default component$(() => {
  const dadJokeSignal = useDadJoke();
  return (
    <div class="section bright">
      <div>{dadJokeSignal.value.joke}</div>
    </div>
  );
});

Posting Data to Server

Previously we used routeLoader$ to send data from the server to the client. We use routeAction$ to post (send) data from the client back to the server.

NOTE: routeAction$ is the preferred way to send data to the server because it uses the browser native form API which works even if JavaScript is disabled.

To declare an action:

  1. Open src/routes/joke/index.tsx and add this code:
import {routeAction$, Form} from "@builder.io/qwik-city";

const useJokeVoteAction = routeAction$((props) => {
  // Leave it as an exercise for the reader to implement this.
  console.log("VOTE", props);
});
  1. Update the export default component to use the useJokeVoteAction hook. This is done by adding the <Form>.
export default component$(() => {
  const dadJokeSignal = useDadJoke();
  const favoriteJokeAction = useJokeVoteAction();
  return (
    <div class="section bright">
      <div>{dadJokeSignal.value.joke}</div>
      <Form action={favoriteJokeAction}>
        <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
        <button name="vote" value="up">
          ๐Ÿ‘
        </button>
        <button name="vote" value="down">
          ๐Ÿ‘Ž
        </button>
      </Form>
    </div>
  );
});
  1. Navigate to http://127.0.0.1:5173/ to see the application running.

What does the above code do?

  • routeAction$ is used to receive data.
    • The function passed to routeAction$ is invoked on the server whenever the form is posted.
    • The routeAction$ returns a use-hook (useJokeVoteAction) that can be used in the component post the form data.
  • Form is a convenience component that wraps the browser's native <form> element.

Things to note:

  • For validation see zod validation.
  • The routeAction$ works even if JavaScript is disabled.
  • If JavaScript is enabled the Form component will prevent the browser from posting the form and instead post the data using JavaScript and emulate the browser's native form behavior without full refresh.
CHECKPOINT: src/routes/joke/index.tsx:
import { component$ } from "@builder.io/qwik";
import {
  routeLoader$,
  Form,
  routeAction$,
  server$,
} from "@builder.io/qwik-city";

const useDadJoke = routeLoader$(async () => {
  const response = await fetch("https://icanhazdadjoke.com/", {
    headers: { Accept: "application/json" },
  });
  return (await response.json()) as {
    id: string;
    status: number;
    joke: string;
  };
});

const useJokeVoteAction = routeAction$((props) => {
  console.log("VOTE", props);
});

export default component$(() => {
  const dadJokeSignal = useDadJoke();
  const favoriteJokeAction = useJokeVoteAction();
  return (
    <div class="section bright">
      <div>
        {dadJokeSignal.value.joke}
      </div>
      <Form action={favoriteJokeAction}>
        <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
        <button name="vote" value="up">
          ๐Ÿ‘
        </button>
        <button name="vote" value="down">
          ๐Ÿ‘Ž
        </button>
      </Form>
    </div>
  );
});

Modifying State

Keeping track of the state and updating UI is core to what applications do. Qwik provides a useSignal hook to keep track of the application's state. To learn more see state management.

To declare state:

  1. Declare the component's state using useSignal().
     const isFavoriteSignal = useSignal(false);
    
  2. Add a button to the component that will modify the state.
    <button onClick={() => isFavoriteSignal.value = !isFavoriteSignal.value}>
      {isFavoriteSignal.value ? "โค๏ธ" : "๐Ÿค"}
    </button>
    

NOTE:

  • Clicking on the button will update the state and the UI will be updated.
CHECKPOINT: src/routes/joke/index.tsx:
import { component$, useSignal } from "@builder.io/qwik";
import {
  routeLoader$,
  Form,
  routeAction$,
  server$,
} from "@builder.io/qwik-city";

const useDadJoke = routeLoader$(async () => {
  const response = await fetch("https://icanhazdadjoke.com/", {
    headers: { Accept: "application/json" },
  });
  return (await response.json()) as {
    id: string;
    status: number;
    joke: string;
  };
});

const useJokeVoteAction = routeAction$((props) => {
  console.log("VOTE", props);
});

export default component$(() => {
  const isFavoriteSignal = useSignal(false);
  const dadJokeSignal = useDadJoke();
  const favoriteJokeAction = useJokeVoteAction();
  return (
    <div class="section bright">
      <div>{dadJokeSignal.value.joke}</div>
      <Form action={favoriteJokeAction}>
        <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
        <button name="vote" value="up">
          ๐Ÿ‘
        </button>
        <button name="vote" value="down">
          ๐Ÿ‘Ž
        </button>
      </Form>
      <button
        onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
      >
        {isFavoriteSignal.value ? "โค๏ธ" : "๐Ÿค"}
      </button>
    </div>
  );
});

Tasks and Invoking Server Code

In Qwik, a task is work that needs to happen when a state changes. (Similar to an "effect" in other frameworks.) In this example, we will use the task to invoke code on the server.

  1. Create a new task that tracks the isFavoriteSignal state:
     useTask$(({ track }) => {
     });
    
  2. Add a track call to the re-executed the task on isFavoriteSignal state change:
     useTask$(({ track }) => {
       track(isFavoriteSignal);
     });
    
  3. Add the work that you want to execute on state change:
     useTask$(({ track }) => {
       track(isFavoriteSignal);
       console.log("FAVORITE (isomorphic)", isFavoriteSignal.value);
     });
    
  4. If you want to have work happen on the server only, wrap it in server$()
    useTask$(({ track }) => {
      track(isFavoriteSignal);
      console.log("FAVORITE (isomorphic)", isFavoriteSignal.value);
      server$(() => {
       console.log("FAVORITE (server)", isFavoriteSignal.value);
      })();
    });
    

NOTE:

  • Notice that the body of useTask$ is executed on both the server and the client (isomorphic).
  • On SSR server prints FAVORITE (isomorphic) false and FAVORITE (server) false.
  • When the user interacts with favorite, the client prints FAVORITE (isomorphic) true and the server prints FAVORITE (server) false.
CHECKPOINT: src/routes/joke/index.tsx:
import { component$, useSignal, useTask$ } from "@builder.io/qwik";
import {
  routeLoader$,
  Form,
  routeAction$,
  server$,
} from "@builder.io/qwik-city";

const useDadJoke = routeLoader$(async () => {
  const response = await fetch("https://icanhazdadjoke.com/", {
    headers: { Accept: "application/json" },
  });
  return (await response.json()) as {
    id: string;
    status: number;
    joke: string;
  };
});

const useJokeVoteAction = routeAction$((props) => {
  console.log("VOTE", props);
});

export default component$(() => {
  const isFavoriteSignal = useSignal(false);
  const dadJokeSignal = useDadJoke();
  const favoriteJokeAction = useJokeVoteAction();
  useTask$(({ track }) => {
    track(isFavoriteSignal);
    console.log("FAVORITE (isomorphic)", isFavoriteSignal.value);
    server$(() => {
      console.log("FAVORITE (server)", isFavoriteSignal.value);
    })();
  });
  return (
    <div class="section bright">
      <div>{dadJokeSignal.value.joke}</div>
      <Form action={favoriteJokeAction}>
        <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
        <button name="vote" value="up">
          ๐Ÿ‘
        </button>
        <button name="vote" value="down">
          ๐Ÿ‘Ž
        </button>
      </Form>
      <button
        onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
      >
        {isFavoriteSignal.value ? "โค๏ธ" : "๐Ÿค"}
      </button>
    </div>
  );
});

Styling

Styling is an important part of any application. Qwik provides a way to associate and scope styles with your component.

To add styles:

  1. Create a new file src/routes/joke/index.css:
    div {
      font-weight: bold;
    }
    
    form {
      float: right;
    }
    
  2. import the styles in src/routes/joke/index.tsx:
    import STYLES from "./index.css?inline";
    
  3. Tell the component to load the styles:
    useStylesScoped$(STYLES);
    

NOTE:

  • The ?inline query parameter tells Vite to inline the styles into the component.
  • The useStylesScoped$ call tells Qwik to associate the styles with the component only (scoping).
  • Styles are only loaded if they are not already inlined as part of SSR and only for the first component.
CHECKPOINT: src/routes/joke/index.css:
div {
  font-weight: bold;
}

form {
  float: right;
}
CHECKPOINT: src/routes/joke/index.tsx:
import {
  component$,
  useSignal,
  useStylesScoped$,
  useTask$,
} from "@builder.io/qwik";
import {
  routeLoader$,
  Form,
  routeAction$,
  server$,
} from "@builder.io/qwik-city";
import STYLES from "./index.css?inline";

const useDadJoke = routeLoader$(async () => {
  const response = await fetch("https://icanhazdadjoke.com/", {
    headers: { Accept: "application/json" },
  });
  return (await response.json()) as {
    id: string;
    status: number;
    joke: string;
  };
});

const useJokeVoteAction = routeAction$((props) => {
  console.log("VOTE", props);
});

export default component$(() => {
  useStylesScoped$(STYLES);
  const isFavoriteSignal = useSignal(false);
  const dadJokeSignal = useDadJoke();
  const favoriteJokeAction = useJokeVoteAction();
  useTask$(({ track }) => {
    track(isFavoriteSignal);
    console.log("FAVORITE (isomorphic)", isFavoriteSignal.value);
    server$(() => {
      console.log("FAVORITE (server)", isFavoriteSignal.value);
    })();
  });
  return (
    <div class="section bright">
      <div>{dadJokeSignal.value.joke}</div>
      <Form action={favoriteJokeAction}>
        <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
        <button name="vote" value="up">
          ๐Ÿ‘
        </button>
        <button name="vote" value="down">
          ๐Ÿ‘Ž
        </button>
      </Form>
      <button
        onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
      >
        {isFavoriteSignal.value ? "โค๏ธ" : "๐Ÿค"}
      </button>
    </div>
  );
});

Preview

We build a very simple application that gave you an overview of key Qwik concepts and API. The application is running in dev mode, which uses hot-module-reloading (HMR) to continuously update the application while changing the code.

While in dev mode:

  • Each file is loaded individually, which may cause waterfalls in the network tab.
  • There is no speculative loading of bundles, so there may be a delay on the first interaction.

Let's create a production build to see how the application will be delivered to the user and how the above is fixed.

To create a preview build:

  1. Run npm run preview to create a production build.

NOTE:

  • Your application should have a production build now and be running on a different port.
  • If you interact with the application now, the network tab of the dev tools should show that the bundles are instantly delivered from the ServiceWorker cache.

Review

Congratulations! You have made it through getting started. This overview is intentionally short to get you familiarized with different parts of Qwik. We recommended that you deep dive into the above concepts to learn more. Here are some key takeaways:

Made with โค๏ธ by

ยฉ 2023 Builder.io, Inc.