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.
Loading tweet…
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:
Loading tweet…
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:
- Don’t mix React and Qwik components in the same file.
- Place all your React code inside the
src/integrations/react
folder. - Add
/** @jsxImportSource react */
at the top of the files containing React code. - 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?
- 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 😍
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:
Note that className
is still a valid prop (e.g. on React components), but not necessary on HTMLElement
s.
However, if using /** @jsxImportSource react */
then you have to continue using className
to avoid this error:
Warning: Invalid DOM property
class
. Did you meanclassName
?
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
:
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.
Lack of next/link
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