Skip to content
All essays
WebMarch 27, 202614 min

Qwik Fundamentals: Building Resumable Web Applications

Master Qwik fundamentals and learn how to build resumable web applications with minimal JavaScript and instant loading.

Ü
Ümit Uz
Mobile & Full Stack Developer

Qwik is a revolutionary framework that rethinks how web applications are built and delivered. By introducing the concept of resumability, Qwik achieves instant page loads and exceptional performance without requiring hydration on the client.

What is Qwik?

Qwik is a new kind of framework that delivers lightning-fast page loads by shipping minimal JavaScript and resuming application state where the server left off. Unlike traditional frameworks that require downloading and executing the entire framework on the client, Qwik breaks applications into tiny, lazy-loaded chunks.

Key Features

  • Resumability: Continue where the server left off
  • Zero Hydration: No framework execution on client startup
  • Lazy Everything: Components, event handlers, and data loading are lazy by default
  • Fine-Grained Reactivity: Updates only what changes
  • Edge-Ready: Runs on any JavaScript runtime
  • Familiar Syntax: React-like component model

Installation and Setup

Creating a New Qwik App

bash
npm create qwik@latest
cd my-qwik-app
npm install
npm run dev

Project Structure

my-qwik-app/
├── src/
│   ├── components/
│   ├── routes/
│   ├── entry.ssr.tsx
│   ├── root.tsx
│   └── global.css
├── public/
├── package.json
└── tsconfig.json

Qwik Components

Basic Component Structure

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

export default component$(() => {
  return <div>Hello Qwik!</div>;
});

The $ Suffix

Qwik uses the $ suffix to mark things for lazy loading:

tsx
import { component$, useSignal } from '@builder.io/qwik';

export const Counter = component$(() => {
  const count = useSignal(0);

  return (
    <div>
      <span>Count: {count.value}</span>
      <button onClick$={() => count.value++}>
        Increment
      </button>
    </div>
  );
});

Component Best Practices

  1. 1Always Use component$: Marks component for lazy loading
  2. 2Use $ for Event Handlers: Lazy loads handler code
  3. 3Keep Components Focused: Single responsibility
  4. 4Type Props: Leverage TypeScript
tsx
interface CardProps {
  title: string;
  description: string;
}

export const Card = component$((props: CardProps) => {
  return (
    <div class="card">
      <h2>{props.title}</h2>
      <p>{props.description}</p>
    </div>
  );
});

State Management

useSignal

Signals hold reactive state:

tsx
import { component$, useSignal } from '@builder.io/qwik';

export default component$(() => {
  const count = useSignal(0);
  const text = useSignal('');

  return (
    <div>
      <input
        value={text.value}
        onInput$={(e) => {
          text.value = (e.target as HTMLInputElement).value;
        }}
      />
      <p>Typed: {text.value}</p>
      <button onClick$={() => count.value++}>
        Count: {count.value}
      </button>
    </div>
  );
});

useStore

For complex state, use stores:

tsx
import { component$, useStore } from '@builder.io/qwik';

interface User {
  name: string;
  email: string;
}

export default component$(() => {
  const user = useStore<User>({
    name: 'John Doe',
    email: 'john@example.com',
  });

  return (
    <div>
      <input
        value={user.name}
        onInput$={(e) => {
          user.name = (e.target as HTMLInputElement).value;
        }}
      />
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
    </div>
  );
});

useComputed

Create derived state:

tsx
import { component$, useSignal, useComputed$ } from '@builder.io/qwik';

export default component$(() => {
  const firstName = useSignal('John');
  const lastName = useSignal('Doe');

  const fullName = useComputed$(() => {
    return `${firstName.value} ${lastName.value}`;
  });

  return (
    <div>
      <input
        value={firstName.value}
        onInput$={(e) => {
          firstName.value = (e.target as HTMLInputElement).value;
        }}
      />
      <p>Full Name: {fullName.value}</p>
    </div>
  );
});

Conditional Rendering

Show Component

tsx
import { component$, useSignal, Show } from '@builder.io/qwik';

export default component$(() => {
  const isLoggedIn = useSignal(false);

  return (
    <Show
      when={isLoggedIn.value}
      fallback={<button onClick$={() => (isLoggedIn.value = true)}>Login</button>}
    >
      <div>
        <h1>Welcome back!</h1>
        <button onClick$={() => (isLoggedIn.value = false)}>Logout</button>
      </div>
    </Show>
  );
});

Multiple Conditions

tsx
import { component$, useSignal, Show, When } from '@builder.io/qwik';

export default component$(() => {
  const status = useSignal<'loading' | 'success' | 'error'>('loading');

  return (
    <Show
      when={status.value === 'loading'}
      fallback={
        <Show
          when={status.value === 'success'}
          fallback={<div>Error loading data</div>}
        >
          <div>Data loaded successfully!</div>
        </Show>
      }
    >
      <div>Loading...</div>
    </Show>
  );
});

List Rendering

For Component

tsx
import { component$, useSignal, For } from '@builder.io/qwik';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

export default component$(() => {
  const todos = useSignal<Todo[]>([
    { id: 1, text: 'Learn Qwik', completed: false },
    { id: 2, text: 'Build an app', completed: false },
  ]);

  return (
    <ul>
      <For each={todos.value}>
        {(todo) => (
          <li>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange$={() => {
                todo.completed = !todo.completed;
              }}
            />
            <span>{todo.text}</span>
          </li>
        )}
      </For>
    </ul>
  );
});

Key Prop

For efficient updates, use keys:

tsx
<For each={items.value}>
  {(item, index) => (
    <div key={item.id}>
      {item.name}
    </div>
  )}
</For>

Event Handling

Event Listeners

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

export default component$(() => {
  const handleClick$ = () => {
    console.log('Button clicked!');
  };

  const handleInput$ = (e: Event) => {
    const input = e.target as HTMLInputElement;
    console.log('Input value:', input.value);
  };

  return (
    <div>
      <button onClick$={handleClick$}>Click Me</button>
      <input onInput$={handleInput$} />
    </div>
  );
});

Event Modifiers

tsx
export default component$(() => {
  return (
    <div>
      <button onClick$={(e) => {
        e.preventDefault();
        console.log('Button clicked, but form not submitted');
      }}>
        Submit
      </button>

      <input
        onKeyDown$={(e) => {
          if (e.key === 'Enter') {
            console.log('Enter key pressed');
          }
        }}
      />
    </div>
  );
});

Lifecycle Hooks

useTask$

Run code in response to state changes:

tsx
import { component$, useSignal, useTask$ } from '@builder.io/qwik';

export default component$(() => {
  const count = useSignal(0);

  useTask$(({ track }) => {
    track(() => count.value);
    console.log('Count changed:', count.value);
  });

  return (
    <button onClick$={() => count.value++}>
      Count: {count.value}
    </button>
  );
});

useVisibleTask$

Run code when component is visible:

tsx
import { component$, useVisibleTask$ } from '@builder.io/qwik';

export default component$(() => {
  useVisibleTask$(() => {
    console.log('Component is visible in the viewport');
  });

  return <div>Watch me appear</div>;
});

useOnDocument

Listen to document-level events:

tsx
import { component$, useOnDocument, $ } from '@builder.io/qwik';

export default component$(() => {
  useOnDocument(
    'click',
    $((event) => {
      console.log('Document clicked', event);
    })
  );

  return <div>Click anywhere</div>;
});

Props and Children

Passing Props

tsx
import { component$, type PropsOf } from '@builder.io/qwik';

interface ButtonProps {
  variant?: 'primary' | 'secondary';
  onClick$?: () => void;
}

export const Button = component$<ButtonProps>((props) => {
  return (
    <button
      class={`btn btn-${props.variant || 'primary'}`}
      onClick$={props.onClick$}
    >
      {props.children}
    </button>
  );
});

// Usage
export default component$(() => {
  return (
    <Button variant="primary" onClick$={() => console.log('Clicked')}>
      Click Me
    </Button>
  );
});

Children Prop

tsx
interface CardProps {
  title: string;
  children: any;
}

export const Card = component$<CardProps>((props) => {
  return (
    <div class="card">
      <h2>{props.title}</h2>
      <div class="content">{props.children}</div>
    </div>
  );
});

Context API

Creating Context

tsx
import { createContext, useContextProvider, useContext } from '@builder.io/qwik';

interface ThemeContext {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

export const ThemeContext = createContext<ThemeContext>('theme');

export const ThemeProvider = component$(({ children }: { children: any }) => {
  const theme = useSignal<'light' | 'dark'>('light');

  const store = useStore({
    theme: theme.value,
    toggleTheme: $(() => {
      theme.value = theme.value === 'light' ? 'dark' : 'light';
      store.theme = theme.value;
    }),
  });

  useContextProvider(ThemeContext, store);

  return <>{children}</>;
});

// Consuming context
export const ThemedComponent = component$(() => {
  const theme = useContext(ThemeContext);

  return (
    <div class={theme.theme}>
      Current theme: {theme.theme}
      <button onClick$={theme.toggleTheme}>Toggle</button>
    </div>
  );
});

Loading Resources

useResource

Fetch data reactively:

tsx
import { component$, useResource, useSignal } from '@builder.io/qwik';

export default component$(() => {
  const userId = useSignal(1);

  const userResource = useResource$(({ track }) => {
    track(() => userId.value);
    return fetch(`/api/users/${userId.value}`).then(r => r.json());
  });

  return (
    <div>
      <button onClick$={() => userId.value++}>Next User</button>

      <Resource
        value={userResource}
        onPending={() => <div>Loading...</div>}
        onRejected={(error) => <div>Error: {error.message}</div>}
        onResolved={(user) => (
          <div>
            <h1>{user.name}</h1>
            <p>{user.email}</p>
          </div>
        )}
      />
    </div>
  );
});

useSuspense

Show loading state during data fetching:

tsx
import { component$, useResource, useSuspense } from '@builder.io/qwik';

export default component$(() => {
  const productsResource = useResource$(() => {
    return fetch('/api/products').then(r => r.json());
  });

  return (
    <Suspense fallback={<div>Loading products...</div>}>
      <Resource value={productsResource}>
        {(products) => (
          <div>
            {products.map((p: any) => (
              <div key={p.id}>{p.name}</div>
            ))}
          </div>
        )}
      </Resource>
    </Suspense>
  );
});

Server-Side Rendering

Qwik City Integration

Qwik works seamlessly with Qwik City for routing:

tsx
// src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';

export const useProducts = routeLoader$(async () => {
  const response = await fetch('https://api.example.com/products');
  const products = await response.json();
  return products;
});

export default component$(() => {
  const products = useProducts();

  return (
    <div>
      <h1>Products</h1>
      {products.value.map((product: any) => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
});

Best Practices

  1. 1Always Use $ Suffix: Mark components and handlers for lazy loading
  2. 2Minimize Client JavaScript: Keep code on server when possible
  3. 3Use Signals: For reactive state
  4. 4Leverage Server: Load data on server
  5. 5Optimize Imports: Import only what you need
  6. 6Test Performance: Monitor bundle size and load times

Conclusion

Qwik represents a paradigm shift in how we build web applications. By embracing resumability and lazy loading, Qwik delivers exceptional performance without sacrificing developer experience. The familiar React-like syntax combined with innovative concepts makes it easy to adopt while delivering superior results.

Start building with Qwik today and experience the future of web development—where instant page loads and minimal JavaScript are the norm, not the exception.

Enjoyed this essay? Share it.

Related essays

Next essay
Qwik City Data Fetching and Server-Side Rendering