Learn how to ship impactful customer journeys with Builder

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Platform
Developers
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

web development

Resumable React: How to Use React Inside Qwik

January 30, 2023

Written By Yoav Ganbar

Did you know you can leverage almost the entirety of the React ecosystem inside a Qwik application?

Basically, you can build React applications, without ever loading React in the user's browser. Sounds good, doesn't it? Let’s see how this works.

You can download the code in this blog post on GitHub.

Kicking off

First, we need a new Qwik app. To create and bootstrap an app, we can run this command — I chose pnpm as my package manager, but you can do the same command with npm:

pnpm create qwik@latest

Follow the CLI (command line interface) prompts. When you finish, it should look something like this:

a screenshot showing the CLI output of creating a new Qwik app.

Next, we need to add qwik-react so we can wrap our React components with the qwikify$() method:

pnpm run qwik add react

This will once again give us a CLI output that looks like this:

a screenshot showing the CLI output of running pnpm run qwik add react.

The above image shows that this has installed:

  • @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

Most of these packages help showcase Material UI working within Qwik, which needs emotion as a dependency. The real substance is in qwik-react and the react and react-dom dependencies.

Let’s run our app in development mode:

pnpm run dev

Now, if we go to http://localhost:5173/ the initial page renders:

A screenshot of a newly created Qwik app.

For the generated UI, we can go to http://localhost:5173/react to display some React Material UI components inside our Qwik app:

A screenshot of a qwik app using React MaterialUI components.

And if we interact with the components, they all work:

Let’s open up src/integrations/react and check out what the CLI generated:

/** @jsxImportSource react */

import { qwikify$ } from '@builder.io/qwik-react';
import { Button, Slider } from '@mui/material';
import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid';

export const MUIButton = qwikify$(Button);
export const MUISlider = qwikify$(Slider, { eagerness: 'hover' });

export const TableApp = qwikify$(() => {
  const columns: GridColDef[] = [
    { field: 'id', headerName: 'ID', width: 70 },
    { field: 'firstName', headerName: 'First name', width: 130 },
    { field: 'lastName', headerName: 'Last name', width: 130 },
    {
      field: 'age',
      headerName: 'Age',
      type: 'number',
      width: 90,
    },
    {
      field: 'fullName',
      headerName: 'Full name',
      description: 'This column has a value getter and is not sortable.',
      sortable: false,
      width: 160,
      valueGetter: (params: GridValueGetterParams) =>
        `${params.row.firstName || ''} ${params.row.lastName || ''}`,
    },
  ];

  const rows = [
    { id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
    // more rows with same object shape ,removed for brevity.
  ];

  return (
    <>
      <h1>Hello from React</h1>

      <div style={{ height: 400, width: '100%' }}>
        <DataGrid
          rows={rows}
          columns={columns}
          pageSize={5}
          rowsPerPageOptions={[5]}
          checkboxSelection
          disableSelectionOnClick
        />
      </div>
    </>
  );
});

There are a few things to notice here.

The first is /** @jsxImportSource react */ at the top of the file; this instructs the Qwik compiler to use React to produce the JSX.

The second thing is how React components are qwikified:

import { qwikify$ } from '@builder.io/qwik-react';
import { Button, Slider } from '@mui/material';

export const MUIButton = qwikify$(Button);
export const MUISlider = qwikify$(Slider, { eagerness: 'hover' });

The MUIButton is declared as a wrapper for Button from inside the @mui/material package and is exported and ready to use within a Qwik app.

It is important to note that a component wrapped this way is not interactive by default and renders only the markup.

MUISlider has a second argument passed with the { eagerness: 'hover'} option.

This is how we trigger the React hydration step, which makes the component interactive.

There are several ways we can define when hydration is triggered.

The QwikifyOptions are undocumented at the moment of writing, as Qwik is still in beta and the docs are still being worked on. You can find the definitions inside the source code.

For the eagerness property we can pass in one of the following options:

  • load: triggered once the document loads. Good for UI elements that need to be as interactive as soon as possible.
  • visible: hydrate once the component is in the viewport. Good for low-priority UI elements that might be further down the page; that is, below the fold.
  • idle: the component hydrates when the browser becomes idle after every important task has been completed.
  • hover: trigger hydration when the mouse hovers over the element.

Similar to Astro’s client directives, Qwik has the client: JSX property. They are the same as the aforementioned strategies but you can put them on the component’s markup:

export default component$(() => {
  return (
    <>
      <MUISlider client:load></MUISlider>
      <MUISlider client:visible></MUISlider>
      <MUISlider client:idle></MUISlider>
      <MUISlider client:hover></MUISlider>
    </>
  );
});

However, there are three more strategies:

  • client:signal
  • client:event
  • client-only

Let's look at each.

client:signal, based on Qwik’s reactive hook useSignal(), allows hydration whenever a signal’s value becomes true:

export default component$(() => {
  const shouldHydrate = useSignal(false);
  return (
    <>
      <button onClick$={() => (shouldHydrate.value = true)}>Hydrate Slider when click</button>

      <MUISlider client:signal={shouldHydrate}></MUISlider>
    </>
  );
});

client:event triggers when specific DOM events are dispatched:

export default component$(() => {
	const count = useSignal(0);
  return (
    <>
      <p>This slider will only run hydration after a click on it</p>
      <MUIEventSlider
        client:event="click"
        value={count.value}
        onChange$={(_, value) => {
          count.value = value as number;
        }}
      />
    </>
  );
});

client:only: will not run server-side render, only on the browser.

Now that we know the options, let’s see how we can load a React component without actually loading the code.

For this, we’ll create a new route in our application and add the MUIButton component:

// src/routes/no-react/index.tsx

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

export default component$(() => {
  return (
    <div>
      <h2>React component with no React code</h2>
      <MUIButton variant="contained">I'm a button from react</MUIButton>
    </div>
  );
});

If we now navigate to our newly created route we can see that everything renders correctly:

a screenshot of a React component with no React code inside a Qwik app.

What we’ve done in the above example is nice, but we would like the button to actually do something. We want an event listener but without React’s synthetic events.

At first, I thought that this would work:

import { component$, useSignal } from '@builder.io/qwik';
import { MUIButton } from '~/integrations/react/mui';

export default component$(() => {
  const count = useSignal(0);
  return (
    <div>
      <h2>React component with Qwik sigal</h2> // ❌
      <MUIButton
        variant="contained"
        onClick$={() => {
          count.value++;
        }}
      >
        I'm a button from react
      </MUIButton>
      <p>current count is: {count.value}</p>
    </div>
  );
});

My reasoning behind this was that using useSignal() would give me the reactivity I needed, and then an onClick$ on the MUIButton would be enough.

However, there’s a good reason this doesn’t work, as Manu (Qwik core maintainer) explained to me:

Passing an onClick this way will pass down the callback method into React, and then it’s up to it to execute it (with its synthetic event system).

It’s not actually a DOM event listener as I thought it would be.

So, to get this to work we could add one of the client directives, like client:visble. Then we’d have:

import { component$, useSignal } from '@builder.io/qwik';
import { MUIButton } from '~/integrations/react/mui';

export default component$(() => {
  const count = useSignal(0);
  return (
    <div>
      <h2>Visibly hydrated React component</h2>
      <MUIButton
        client:visible
        variant="contained"
        onClick$={() => {
          count.value++;
        }}
      >
        I'm a button from react
      </MUIButton>
      <p>current count is: {count.value}</p>
    </div>
  );
});

This works. However, notice from the video below, React is loaded, as my React DevTools on the right show:

In order to get the result we want, we need to use the host: attribute, which allows us to listen to DOM events without hydration.

Let’s refactor a bit and see what this looks like:

import { component$, useSignal } from '@builder.io/qwik';
import { MUIButton } from '~/integrations/react/mui';

export default component$(() => {
  const count = useSignal(0);
  return (
    <div>
      <h2>React component with no React code</h2>
      <MUIButton
        // we removed the hydration directive
        variant="contained"
        // and just added "host:" in front of our onClick$
        host:onClick$={() => {
          count.value++;
        }}
      >
        I'm a button from react
      </MUIButton>
      <p>current count is: {count.value}</p>
    </div>
  );
});

Now it works without React! 🎉🎉🎉

We can infer this since our dev tools are not showing the React DevTools extension as they did before:

One last thing to notice is that now we do not get that nice Material UI ripple effect because it needs to be triggered internally by React code.

But we did get a React component that is pure HTML with reactivity from Qwik signals, which is way less code.

Time to look at some numbers. When I say less code, what do I mean? (This is after building and running locally on my very fast machine.)

Qwikified React component with hydration with signal: 95.6KB of JS over the network.

A screenshot of a Qwik app with a hydrated React component and the devtools network pane on the right side.

For fun, I built the same minimal example with a brand new vite / React project and installed the same React dependencies.

This is the React app code:

import { useState } from 'react';

import './App.css';
import { Button } from '@mui/material';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <div>
        <h2>Regular React component</h2>
        <Button
          variant="contained"
          onClick={() => {
            setCount(count + 1);
          }}
        >
          I'm a button from react
        </Button>
        <p>current count is: {count}</p>
      </div>
    </div>
  );
}

export default App; 

React Vite build is less JS over the network: 71.9KB of JS in one single file.

A screenshot of a React app with the devtools network pane on the right side.

However, it seems that the React app has twice as much time-to-interactive than Qwik running React (on my machine, running lighthouse with a new Arc browser space with no extensions).

screenshot showing a lighthouse score of 0.6s for Qwik app.
screenshot showing a lighthouse score of 1.2s for React app.

Qwikified React component with no react code: 7.7KB

A screenshot of a Qwik app with the devtools network pane on the right that shows only 7.7KB transfer over the network.

At the time of this writing, Qwik is still in beta. So, as APIs are still changing and docs are still being polished there is still some work to be done.

Some things to keep in mind:

  • Every Qwikified React component is isolated — it’s a fully isolated app. It's an island similar to Astro Islands. However, here they hydrate independently.
  • Things can get duplicated, like styles.
  • You cannot inherit React context
  • Interactivity is disabled by default.

Known issues:

  • When playing around with new routes that use qwik-react, you need to restart the server.

In this post:

  • We went over how to load React code inside of a Qwik application.
  • We learned how to make it interactive via the different hydration strategies available.
  • We learned how to make React resumable by loading the React component as HTML and allowing Qwik to handle interactions.

Having the option not to duplicate what you might have done in the past and have the ability to port it to a different foundation is exciting.

I see how this method could be leveraged to migrate React SPAs to Instant Reactive Qwik apps in the future.

To learn more, check out:

Tip: Visit our Qwik hub to learn more.

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
AI8 MIN
How to Build Reliable AI Tools
November 15, 2024
Web Design11 MIN
Design Smarter with Figma Auto Layout
November 13, 2024
Web Development10 MIN
A Guide to Server-Side Rendering
November 12, 2024