Converting Next.js to Qwik

During the holiday break, I decided to convert my site to Spotlight. The main reason was, so I could remove blog pagination, group related content (e.g. last year’s Advent of JS section), and add a CV.

Being a long-time user of Next.js & many Vercel projects, I attempted to migrate /pages to /app via this guide: Migrating from pages to app.

Unfortunately, the issue wasn’t in copy/paste friction, but in converting a template that’s relatively new to me to use entirely different paradigms.

For example, how should I access the file system outside getStaticProps? (My guess is to move the logic into /pages/api/posts.js and fetch that instead.)

My Interest in Qwik

I’ve been casually observing Qwik’s development since I learned that it resumes JS execution from the server.

Years ago (~2015?) at HigherEducation.com, I created monetization “widgets” that were powered by React, but would generate a dynamic, initial state on the server, serialize the minimal HTML/CSS, and progressively enhance it with vanilla JS.

For example:

This is a basic example, but the more complex React widgets’ componentDidMount had vanilla JS that directly manipulated the DOM with events & mutations.

Aggressively pursuing progressive enhancement instead of hydration dropped the bundle-size to 1/150th the size.

Qwik First Impressions

Routing

Qwik’s Directory Layout matches my own intuition of using index.js|mdx|tsx as the page content behind a URL: File system Routers & Indexes

I believe Next.js made a mistake with the /pages convention by making /pages/about.js serve /about. This has created numerous issues, especially with colocation of code, without making a config change:

React Support

A deal-breaker with incremental adoption is not being able to re-use any existing functionality.

Luckily, Qwik does support React via a qwikify$.

Oddly, pnpm qwik add react installs Material UI. I think this is as an example, but shouldn’t be a default:

...
🌟 Create
   - src/routes/react/index.tsx
   - src/integrations/react/mui.tsx

💾 Install pnpm dependencies:
   - @builder.io/qwik-react 0.2.1
   - @emotion/react 11.10.4
   - @emotion/styled 11.10.4
   - @mui/material 5.10.10
   - @mui/x-data-grid 5.17.8
   - @types/react 18.0.14
   - @types/react-dom 18.0.5
   - react 18.2.0
   - react-dom 18.2.0

The key constraints are:

  1. Don’t mix React and Qwik components in the same file.
  2. Place all your React code inside the src/integrations/react folder.
  3. Add /** @jsxImportSource react */ at the top of the files containing React code.
  4. Use qwikify$() to convert React components into Qwik components, which you can import from Qwik modules.

What I’m doing is copy/pasting my entire Next.js src/components/* contents into Qwik’s src/integrations/react/*, then creating Qwik wrappers under src/components/*:

import { component$ } from '@builder.io/qwik'
import { Header } from '~/integrations/react/Header'

export default component$(Header)

I would love if I could do this automatically:

import { Header } from '~/integration/react/Header?qwikify$'

In reality, there seems to be some nuances preventing this.

One edge-case was that, since I’m no longer using next/router, removing this means I needed to replace useRouter() with something like useLocation().

But this Qwik API throws a build error:

[vite] Internal server error: Code(14): Invoking 'use\*()' method outside of invocation context.

My initial take is that, if Tailwind UI had one component per file, it would be easier to separate React from Qwik components!

Another issue with migration was this frustratingly unclear error:

QWIK ERROR Code(25): Invalid JSXNode type. It must be either a function or a string.

Remember step 3?

  1. Add /** @jsxImportSource react */ at the top of the files containing React code.

That means all files. Why can’t this be auto-set for src/integrations/react?

Once I added /** @jsxImportSource react */, the errors went away!

Since interactivity is disabled by default, I initially added client:visible to force hydration on the client:

/** @jsxImportSource react */

import { qwikify$ } from '@builder.io/qwik-react'
import Home from '~/integrations/react/pages/Home'

export default qwikify$(Home, { eagerness: 'visible' })

Click-to-Component

Ever since I prototyped codelift, Option+Click has been a huge DX improvement to go from what you see to the code.

Heck, I even open-sourced click-to-component after engineers at Stripe used it thousands of times a month!

Qwik locally welcomed me with this helpful Click-to-Source prompt 😍 Qwik

Note – This doesn’t work for wrapped React components!

class vs. className

The ship has sailed for JSX 1.0 (maybe 2.0?), but I was surprised when I saw this @deprecated warning:

Qwik className

Note that className is still a valid prop (e.g. on React components), but not necessary on HTMLElements.

However, if using /** @jsxImportSource react */ then you have to continue using className to avoid this error:

Warning: Invalid DOM property class. Did you mean className?

Inline Scripts

This was one of the pain-points for /app in Next.js – <script>...</script> isn’t well-supported!

A Qwik search through their GitHub issues revealed docs: RenderOnce and dangerouslySetInnerHTML – they’ve ditched React’s {{ __html: ... }} eyesore.

Side note: I think this is a case for putting the inline code behind a href="/public/inline.js" instead.

import cannot find index.tsx

For complex components, I like to structure the directory this way:

  • /components
    • /Head
      • index.tsx
      • styles.css
      • ...

Unfortunately, these cannot be found unless you explicitly use ./components/Head/index:

Qwik index

Lack of next/image

I’ve found that next/image is extremely useful for optimizing images based on how they’re used without any manual effort.

I see that Image Optimization is being discussed, so traditional <img> tags are probably the way to go.

Although, I wonder if I can re-use some of my previous Node.js image-optimization code to dynamically generate the resized image 🤔

Since Qwik is bundled via Vite, I may be able to use their Static Asset Handling.

Good riddance next/link 👋🏻

Path Aliases

I’m happy to see ~/* or @/* are common conventions for src/* ❤️

Customizing <head>

Rather than next/head (which I first knew as react-helmet), Qwik customizes head via export const head = {...}.

I prefer this because it’s less about side effects when rendering, and something that can be static or dynamically available without rendering.

Equivalent of getStaticProps?

With a folder of src/routes/blog/**/*.mdx files loaded up, the next hurdle is how to get a list of all blog posts?

I expected @qwik-city-plan to solve this:

import cityPlan from '@qwik-city-plan'

But the only values I’m getting when logging cityPlan is { props: { [Symbol(IMMUTABLE)]: {} } }.

Frontmatter

Even though the docs use Frontmatter, there isn’t explicit support: [DOCS] Frontmatter.

Since vite.config.js is available, then I could customize Qwik’s remark plugins to instead use:

import remarkGfm from 'remark-gfm'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'

...

remarkPlugins: [
  remarkFrontmatter,
  [remarkMdxFrontmatter, { name: 'meta' }],
  ...
]

Last Impressions

At this point, a few hours later, I think Qwik is promising, but premature for my usage.

I enjoyed the onboarding experience, the details & examples in the documentation, and a path for React re-use.

However, I believe it needs a strong “brownfield” development story – incrementally migrating to Qwik indeed has “wide islands” which made me wonder if the effort warranted it.

I was hoping that @qwik-city-plan & useContent() was going to negate my need for reading the file system for blog content.

In practice, Qwik just didn’t seem to “scale” to meet my needs.

PR: https://github.com/ericclemmons/ericclemmons/pull/new/qwik