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 React developers.
To play with it right away, check out our in-browser playgrounds:
- StackBlitz Qwik (Full Qwik + Qwikcity integration)
- Examples playground (Qwik only, no routing)
Prerequisites
To get started with Qwik locally, you need the following:
- Node.js v16.8 or higher
- Your favorite IDE (vscode recommended)
- Optionally, read think qwik
Create an app using the CLI
First, create a Qwik application with the Qwik CLI, which generates a blank starter so that you can quickly familiarize yourself with it.
Run the Qwik CLI in your shell. Qwik supports NPM, yarn and pnpm. 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 guides you through an interactive menu to set the project-name, select one of the starters and asks if you want to install the dependencies. Find out more about the files generated by referring to the Project Structure documentation.
Start the development server:
npm start
pnpm start
yarn start
Qwik Joke App
The Qwik Hello World tutorial guides you through building a joke app with Qwik while covering the most important Qwik concepts. The app displays a random joke from https://icanhazdadjoke.com and features a button to get a new joke on click.
1. Create A Route
Start by serving a page at a particular route. This basic app serves a random dad joke application on the /joke/
route. This tutorial relies on Qwikcity, Qwik's meta-framework, which uses directory-based routing. To get started:
- In your project, create a new
joke
directory inroutes
containing anindex.tsx
file. - Each route's
index.tsx
file must have anexport default component$(...)
so that Qwikcity knows what content to serve. Paste the following content tosrc/routes/joke/index.tsx
:
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <section class="section bright">A Joke!</section>;
});
- 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.
2. Loading Data
We will use the external JSON API at https://icanhazdadjoke.com to load random jokes. We will use route loaders, to load this data in the server and then render it in the component.
- Open
src/routes/joke/index.tsx
and add this code:
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export 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$(() => {
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
</section>
);
});
- Now on
http://localhost:5173/joke/
, the browser displays a random joke.
Code explanation:
- 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.
3. Posting Data to the Server
Previously, we used routeLoader$
to send data from the server to the client. To post (send) data from the client back to the server, we use routeAction$
.
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, add this code:
import { routeAction$, Form } from '@builder.io/qwik-city';
export const useJokeVoteAction = routeAction$((props) => {
// Leave it as an exercise for the reader to implement this.
console.log('VOTE', props);
});
- Update the
export default
component to use theuseJokeVoteAction
hook with<Form>
.
export default component$(() => {
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<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>
</section>
);
});
- Now on
http://localhost:5173/joke/
, the buttons display and if you click them, their value logs to the console.
Code explanation:
routeAction$
receives the data.- The function passed to
routeAction$
is invoked on the server whenever the form is posted. - The
routeAction$
returns a use-hook,useJokeVoteAction
, that you can use in the component to post the form data.
- The function passed to
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 a full refresh.
For reference, the complete code snippet for this section is as follows:
import { component$ } from '@builder.io/qwik';
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
export 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 const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<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>
</section>
);
});
4. Modifying State
Keeping track of the state and updating the 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:
- Import
useSignal
fromqwik
.import { component$, useSignal } from "@builder.io/qwik";
- Declare the component's state using
useSignal()
.const isFavoriteSignal = useSignal(false);
- After the closing
Form
tag, add a button to the component to modify the state.<button onClick$={() => { isFavoriteSignal.value = !isFavoriteSignal.value; }}> {isFavoriteSignal.value ? '❤️' : '🤍'} </button>
NOTE: Clicking on the button updates the state, which in turn updates the UI.
For reference, the complete code snippet for this section is as follows:
import { component$, useSignal } from '@builder.io/qwik';
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
export 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 const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
const isFavoriteSignal = useSignal(false);
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<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>
</section>
);
});
5. Tasks and Invoking Server Code
In Qwik, a task is work that needs to happen when a state changes. (This is similar to an "effect" in other frameworks.) In this example, we use the task to invoke code on the server.
- Create a new task that tracks the
isFavoriteSignal
state:useTask$(({ track }) => {});
- Add a
track
call to re-execute the task onisFavoriteSignal
state change:useTask$(({ track }) => { track(()=> isFavoriteSignal.value); });
- Add the work that you want to execute on state change:
useTask$(({ track }) => { track(()=> isFavoriteSignal.value); console.log('FAVORITE (isomorphic)', isFavoriteSignal.value); });
- If you want to have work happen on the server only, wrap it in
server$()
useTask$(({ track }) => { track(()=> isFavoriteSignal.value); console.log('FAVORITE (isomorphic)', isFavoriteSignal.value); server$(() => { console.log('FAVORITE (server)', isFavoriteSignal.value); })(); });
NOTE:
- The body of
useTask$
is executed on both the server and the client (isomorphic). - On SSR, the server prints
FAVORITE (isomorphic) false
andFAVORITE (server) false
. - When the user interacts with favorite, the client prints
FAVORITE (isomorphic) true
and the server printsFAVORITE (server) true
.
For reference, the complete code snippet for this section is as follows:
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import {
routeLoader$,
Form,
routeAction$,
server$,
} from '@builder.io/qwik-city';
export 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 const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
const isFavoriteSignal = useSignal(false);
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
server$(() => {
console.log('FAVORITE (server)', isFavoriteSignal.value);
})();
});
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<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>
</section>
);
});
6. Styling
Styling is an important part of any application. Qwik provides a way to associate and scope styles with your component.
To add styles:
-
Create a new file
src/routes/joke/index.css
:p { font-weight: bold; } form { float: right; }
-
import the styles in
src/routes/joke/index.tsx
:import styles from "./index.css?inline";
-
Tell the component to load the styles:
useStylesScoped$(styles);
Code explanation:
- 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.
For reference, the complete code snippet for this section is as follows:
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';
export 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 const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
useStylesScoped$(styles);
const isFavoriteSignal = useSignal(false);
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
server$(() => {
console.log('FAVORITE (server)', isFavoriteSignal.value);
})();
});
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<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>
</section>
);
});
7. Preview
We built a minimal 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 that eliminates these issues.
To create a preview build:
- 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've learned a lot about Qwik! For more on just how much you can achieve with Qwik, we recommend reading the dedicated docs on each of the topics touched on in this tutorial: