Migrating to Qwik v2

Qwik v2 is a ground-up rewrite of the framework. This guide covers what's new, how to run the automated migration, and what to verify afterward.

What's New in v2

  • Vite environment API — Better monorepo and adapter support
  • Vite 8 / Rolldown — Out of the box compatibility
  • Smaller serialized state — Up to 30% smaller HTML
  • HMR — Instant browser updates without losing state
  • useAsync$ — Replaces useResource$ with polling, concurrency control, and abort

Quick Start

  1. Run the CLI
  2. Handle third-party libraries
  3. Check behavioral changes
  4. Migrate deprecated APIs
  5. Run the checklist

Run the Migration CLI

pnpm qwik migrate-v2

The CLI handles package renames, identifier changes, config updates, and dependency migration automatically.


Third-Party Libraries

If you use third-party libraries that depend on @builder.io/qwik, you may need to configure overrides and SSR bundling.

Package manager overrides

Redirect the old package name so your package manager doesn't install v1 alongside v2:

{
  "pnpm": {
    "overrides": {
      "@builder.io/qwik": "npm:@qwik.dev/core@^2",
      "@builder.io/qwik-city": "npm:@qwik.dev/router@^2"
    }
  }
}

ssr.noExternal

Qwik libraries must be bundled into the server build for the optimizer to process them:

// vite.config.ts
export default defineConfig({
  ssr: {
    noExternal: ['some-qwik-library'],
  },
  optimizeDeps: {
    exclude: ['some-qwik-library'],
  },
});
NOTEWithout this, you'll see Code(Q30) duplicate runtime errors or "external dependency" warnings.

Behavioral Changes

These won't cause compile errors but will break runtime behavior if not addressed.

useComputed$ is sync-only

useComputed$ rejects async functions in v2. Error Q29 fires at runtime.

// v1 (no longer supported in v2)
const data = useComputed$(async () => {
  const result = await fetch('/api/data');
  return result.json();
});
// v2
const data = useAsync$(async ({ abortSignal }) => {
  const result = await fetch('/api/data', { signal: abortSignal });
  return result.json();
});
READING ASYNCSIGNAL.VALUE

.value throws while unresolved. Always branch on .loading / .error first, or provide an initial value.

let content: JSXOutput;
 
if (data.loading) {
  content = <p>Loading...</p>;
} else if (data.error) {
  content = <p>Error: {data.error.message}</p>;
} else {
  content = <p>{JSON.stringify(data.value)}</p>;
}
 
return <div>{content}</div>;

useVisibleTask$ eagerness removed

The eagerness option ('load' / 'idle') was removed in v2. Delete it if present.

QwikCityProvider → useQwikRouter

Replace the <QwikCityProvider> wrapper with the useQwikRouter() hook:

// v1
import { QwikCityProvider, RouterOutlet } from '@builder.io/qwik-city';
 
export default component$(() => {
  return (
    <QwikCityProvider>
      <head>
        <meta charset="utf-8" />
      </head>
      <body>
        <RouterOutlet />
      </body>
    </QwikCityProvider>
  );
});
// v2
import { RouterOutlet, useQwikRouter } from '@qwik.dev/router';
 
export default component$(() => {
  useQwikRouter();
 
  return (
    <>
      <head>
        <meta charset="utf-8" />
      </head>
      <body>
        <RouterOutlet />
      </body>
    </>
  );
});
NOTEIf your root component is reactive (reads signals), use <QwikRouterProvider> instead. useQwikRouter() only runs once during SSR.

Serialization

v1 serialized state into <script type="qwik/json"> tags. v2 uses <script type="qwik/vnode"> and <script type="qwik/state"> at the end of the document. No code change needed, but tooling that parses the old tags will need updating.


Deprecated APIs

Still compile in v2, removed in v3.

useResource$ → useAsync$

Aspectv1 useResource$v2 useAsync$
Return type.value: Promise<T>.value: T
Track depsctx.track(() => sig.value)ctx.track(sig) (both forms work)
AbortManual AbortControllerctx.abortSignal
Previous value-ctx.previous
Polling-options.interval
Initial value-options.initial
Rendering<Resource onResolved={} />if/else branching

What changed:

  • track(() => signal.value)track(signal) (shorthand, old form still works)
  • Manual AbortController + cleanup()ctx.abortSignal
  • <Resource onResolved={} />if/else branching
  • .value is T directly, not Promise<T>
  • .error is Error | undefined
  • For unlimited parallel fetches, pass { concurrency: 0 }

qwik-labs

@builder.io/qwik-labs is removed in v2:

Featurev2 Replacement
Insights@qwik.dev/core/insights + @qwik.dev/core/insights/vite
Typed RoutesBuilt into @qwik.dev/router via qwikTypes()

Troubleshooting

Find your error message below.

useComputed$ QRL ... cannot return a Promise

Replace with useAsync$. See useComputed$ is sync-only.

Only primitive and object literals can be serialized

A class instance or plain function in a store/signal/prop (error Q3). Wrap with noSerialize() or convert to a QRL with $().

Qwik version X already imported while importing Y

Two copies of Qwik loaded (error Q30). Add package manager overrides and ssr.noExternal.

IMPORTANT: This dependency was pre-bundled by Vite

Add the library to optimizeDeps.exclude. See ssr.noExternal.

[package] is being treated as an external dependency

Add to both ssr.noExternal and optimizeDeps.exclude. See ssr.noExternal.

Cannot find module '@builder.io/qwik'

Usually a stale jsxImportSource in tsconfig.json. Run the CLI again or check your tsconfig.json.

Cannot find module '@builder.io/qwik-labs'

Package removed in v2. See qwik-labs.

ERR_REQUIRE_ESM / require() of ES Module

Add "type": "module" to package.json. Run the CLI again if this wasn't set automatically.

Calling a 'use*()' method outside 'component$(...)'

Move the hook inside component$ (error Q10).

Move qwik packages [...] to devDependencies

Move all @qwik.dev/* to devDependencies in package.json.


Verification Checklist

Every item should pass before your migration is done.

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • thejackshelton