Qwik React ⚛️

QwikReact allows you to use React components in Qwik, including the whole ecosystem of component libraries, such as Material UI, Threejs and React Spring.

Installation

Inside your Qwik app run:

npm run qwik add react

If you don't have a Qwik app yet, then you need to create one first, then, follow the instructions and run the command add React to your app.

npm create qwik
cd to-my-app
npm run qwik add react

Usage

The @builder.io/qwik-react package exports the qwikify$() function that lets you convert React components into Qwik components, that you can use across your application.

Note: You CAN NOT use React components in Qwik without converting them first, using qwikify$(). Even though React and Qwik component look similar, they are fundamentally very different.

React and Qwik components can not be mixed in the same file, if you check your project right after running the installation command, you will see a new folder src/integrations/react/, from now on, all your React components will live there.

src/integrations/react/mui.tsx:

/** @jsxImportSource react */

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

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

Important: You need to import /** @jsxImportSource react */ at the top of your file, this is an instruction to the compiler to use React as the JSX factory.

In a nutshell, the rules 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.

Now your Qwik can import MUIButton and use it as any other Qwik component:

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

export default component$(() => {
  return (
    <>
      <MUIButton client:hover>Hello this is a button</MUIButton>

      <MUIAlert severity="warning">This is a warning from Qwik</MUIAlert>
    </>
  );
});

qwikify$()

The qwikify$(ReactCmp, options?): QwikCmp allows to implement partial hydration of React components. It works by wrapping the SSR and hydration logic of React into a Qwik component that can execute React's renderToString() during SSR and dynamically call hydrateRoot() when specified.

Notice that by default no React code will run in the browser, meaning that React component will NOT be interactive by default, for example, in the following example, we qwikify the Slider component from MUI, but it will not be interactive.

/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { Slider } from '@mui/material';
export const MUISlider = qwikify$(Slider);
import { component$ } from '@builder.io/qwik';
import { MUISlider } from '~/integrations/react/mui';

export default component$(() => {
  return (
    <>
      <MUISlider></MUISlider>
    </>
  );
});

Limitations

Every qwikified react component is isolated

Each instance of a qwikified react component becomes an independent React app. Fully isolated.

export const MUISlider = qwikify$(Slider);

<MUISlider></MUISlider>
<MUISlider></MUISlider>
  • Each MUISlider is a fully isolated React application, with its own state, lifecycle, etc.
  • Styles will be duplicated
  • State will not be shared
  • Context will not be inherited.
  • Islands will hydrate independently

By default interactivity is disabled

By default, qwikified components will not be interactive, please look at the next section to learn how to enable interactivity.

Use qwikify$() as a migration strategy

Using React components in Qwik is a great way to migrate your application to Qwik, but it's not a silver bullet, you will need to rewrite your components to take advantage of Qwik's features.

It's also a great way to enjoy the React ecosystem, like threejs or data-grid libs.

Dont abuse of qwikify$() to build your own application, all performance gains will be lost.

Build wide islands, not leaf nodes

For example, if you need to use several MUI components, to build a list, dont qwikify each individual MUI component, instead, build the whole list as a single qwikified React component.

GOOD: Wide island

A single qwikified component, with all the MUI components inside. Styles will not be duplicated, and context and theming will work as expected.

import * as React from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar';
import ImageIcon from '@mui/icons-material/Image';
import WorkIcon from '@mui/icons-material/Work';
import BeachAccessIcon from '@mui/icons-material/BeachAccess';

// Qwikify the whole list
export const FolderList = qwikify$(() {
  return (
    <List sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
      <ListItem>
        <ListItemAvatar>
          <Avatar>
            <ImageIcon />
          </Avatar>
        </ListItemAvatar>
        <ListItemText primary="Photos" secondary="Jan 9, 2014" />
      </ListItem>
      <ListItem>
        <ListItemAvatar>
          <Avatar>
            <WorkIcon />
          </Avatar>
        </ListItemAvatar>
        <ListItemText primary="Work" secondary="Jan 7, 2014" />
      </ListItem>
      <ListItem>
        <ListItemAvatar>
          <Avatar>
            <BeachAccessIcon />
          </Avatar>
        </ListItemAvatar>
        <ListItemText primary="Vacation" secondary="July 20, 2014" />
      </ListItem>
    </List>
  );
});

BAD: Leaf nodes

Leaf nodes are qwikified independently, effectively rendering dozens of nested react applications, each fully isolated from the others, and styles being duplicated.

import * as React from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar';
import ImageIcon from '@mui/icons-material/Image';
import WorkIcon from '@mui/icons-material/Work';
import BeachAccessIcon from '@mui/icons-material/BeachAccess';

export const MUIList = qwikify$(List);
export const MUIListItem = qwikify$(ListItem);
export const MUIListItemText = qwikify$(ListItemText);
export const MUIListItemAvatar = qwikify$(ListItemAvatar);
export const MUIAvatar = qwikify$(Avatar);
export const MUIImageIcon = qwikify$(ImageIcon);
export const MUIWorkIcon = qwikify$(WorkIcon);
export const MUIBeachAccessIcon = qwikify$(BeachAccessIcon);
// Qwik component using dozens of nested React islands
// Each MUI-* it's an independent React application
export const FolderList = component$(() {
  return (
    <MUIList sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
      <MUIListItem>
        <MUIListItemAvatar>
          <MUIAvatar>
            <MUIImageIcon />
          </MUIAvatar>
        </MUIListItemAvatar>
        <MUIListItemText primary="Photos" secondary="Jan 9, 2014" />
      </MUIListItem>
      <MUIListItem>
        <MUIListItemAvatar>
          <MUIAvatar>
            <MUIWorkIcon />
          </MUIAvatar>
        </MUIListItemAvatar>
        <MUIListItemText primary="Work" secondary="Jan 7, 2014" />
      </MUIListItem>
      <MUIListItem>
        <MUIListItemAvatar>
          <MUIAvatar>
            <MUIBeachAccessIcon />
          </MUIAvatar>
        </MUIListItemAvatar>
        <MUIListItemText primary="Vacation" secondary="July 20, 2014" />
      </MUIListItem>
    </MUIList>
  );
});

Adding interactivity

In order to add interactivity, in React terminology we need to hydrate, usually in React applications this hydration task happens unconditionally at load time, adding a massive overhead and making sites slow.

Qwik allows you decide when to hydrate your components, by using the client: JSX properties, this technique is commonly called partial hydration, popularized by Astro.

export default component$(() => {
  return (
    <>
-      <MUISlider></MUISlider>
+      <MUISlider client:visible></MUISlider>
    </>
  );
});

Qwik comes with different strategies out of the box:

client:load

The component eagerly hydrates when the document loads.

<MUISlider client:load></MUISlider>

Use case: Immediately-visible UI elements that need to be interactive as soon as possible.

client:idle

The component eagerly hydrates when the browser first become idle, ie, when everything important as already run before.

<MUISlider client:idle></MUISlider>

Use case: Lower-priority UI elements that don’t need to be immediately interactive.

client:visible

The component eagerly hydrates when it becomes visible in the viewport.

<MUISlider client:visible></MUISlider>

Use case: Low-priority UI elements that are either far down the page (“below the fold”) or so resource-intensive to load that you would prefer not to load them at all if the user never saw the element.

client:hover

The component eagerly hydrates when the mouse is over the component.

<MUISlider client:hover></MUISlider>

Use case: Lowest-priority UI elements which interactivity is not crucial, and only needs to run in desktop.

client:signal

This is an advanced API that allows to hydrate the component whenever the passed signal becomes true.

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

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

This effectively allows you to implement custom strategies for hydration.

client:event

The component eagerly hydrates when specified DOM events are dispatched.

<MUISlider client:event="click"></MUISlider>

client:only

When true, the component will not run in SSR, only in the browser.

<MUISlider client:only></MUISlider>

Listening to React events

Events in React are handled by passing a function as a property to the component, for example:

// React code (won't work in Qwik)

import { Slider } from '@mui/material';

<Slider onChange={() => console.log('value changed')}></Slider>;

The qwikify() function will convert this into a Qwik component that will also expose the React events as Qwik QRLs:

import { Slider } from '@mui/material';
import { qwikify$ } from '@builder.io/qwik-react';
const MUISlider = qwikify$(Slider);

<MUISlider client:visible onChange$={() => console.log('value changed')} />;

Notice that we use the client:visible property to eagerly hydrate the component, otherwise the component would not be interactive and the events would never be dispatched.

Host element

When wrapping a React component with qwikify$(), under the hood, a new DOM element is created, such as:

<qwik-react>
  <button class="MUI-button"></button>
</qwik-react>

Notice, that the tag name of the wrapper element is configurable via tagName: qwikify$(ReactCmp, { tagName: 'my-react' }).

Listen to DOM events without hydration

The host element is not part of React, meaning that hydration is not necessary to listen for events, in order to add custom attributes and events to the host element, you can use the host: prefix in the JSX properties, such as:

<MUIButton
  host:onClick$={() => {
    console.log('click a react component without hydration!!');
  }}
/>

This will effectively allow you to respond to a click in a MUI button without downloading a single byte of React code.

Happy hacking!

Made with ❤️ by