Qwik React ⚛️

Qwik React を使用すると、ReactQwik 内で使用できます。Qwik React を使用する利点は、既存の React コンポーネントとライブラリを Qwik 内で使用できることです。これにより、Material UIThreejsReact Spring など、React コンポーネントとライブラリの大規模なエコシステムを利用できます。また、React アプリケーションを書き直すことなく、Qwik の利点を得るための良い方法でもあります。

基本的な使い方

Qwik React の基本的な使い方は、既存の React コンポーネントを取得し、qwikify$ 関数でラップすることです。この関数は、Qwik 内で使用できる Qwik コンポーネントを作成し、React コンポーネントをアイランドに変えて、React コンポーネントをいつハイドレーションするかを微調整する自由を与えます。

基本的な使い方

// This pragma is required so that React JSX is used instead of Qwik JSX
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
 
// An existing React component
function Greetings() {
  return <div>Hello from React</div>;
}
 
// Qwik component wrapping the React component
export const QGreetings = qwikify$(Greetings);

0. インストール

Qwik React を使用する前に、Qwik プロジェクトが Qwik React を使用するように構成する必要があります。最も簡単な方法は、次のコマンドを実行することです。

まだ Qwik アプリがない場合は、最初に 作成 する必要があり、その後、指示に従って、アプリに React を追加するコマンドを実行します。

npm run qwik add react

上記のコマンドは、以下を実行します。

  1. package.json に必要な依存関係をインストールします
    {
     ...,
      "dependencies": {
       ...,
        "@builder.io/qwik-react": "0.5.0",
        "@types/react": "18.0.28",
        "@types/react-dom": "18.0.11",
        "react": "18.2.0",
        "react-dom": "18.2.0",
      }
    }

    : これは React のエミュレーションではありません。実際の React ライブラリを使用しています。

  2. @builder.io/qwik-react プラグインを使用するように Vite を構成します
    // vite.config.ts
    import { qwikReact } from '@builder.io/qwik-react/vite';
     
    export default defineConfig(() => {
       return {
         ...,
         plugins: [
           ...,
           // The important part
           qwikReact()
         ],
       };
    });

: npm run qwik add react は、Qwik React 統合を紹介するデモルートも構成します。これらは

  • package.json dependencies
    • @emotion/react 11.10.6
    • @emotion/styled 11.10.6
    • @mui/material 5.11.9
    • @mui/x-data-grid 5.17.24
  • src/route:
    • /src/routes/react: React 統合を紹介する新しいパブリックルート
    • /src/integrations/react: ここに React コンポーネントがあります

このガイドではこれらを無視し、代わりに最初からプロセスを説明します。

1. Hello World

簡単な例から始めましょう。簡単な React コンポーネントを作成し、それを Qwik コンポーネントでラップします。次に、Qwik ルートで Qwik コンポーネントを使用します。

react.tsxindex.tsx
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
 
// Create React component standard way
function Greetings() {
  return <p>Hello from React</p>;
}
 
// Convert React component to Qwik component
export const QGreetings = qwikify$(Greetings);

@builder.io/qwik-react パッケージは、React コンポーネントをアプリケーション全体で使用できる Qwik コンポーネントに変換できる qwikify$() 関数をエクスポートします。

注: qwikify$() を使用して最初に変換しないと、Qwik で React コンポーネントを使用することはできません。React コンポーネントと Qwik コンポーネントは似ていますが、根本的に大きく異なります。

ReactとQwikのコンポーネントは同じファイル内で混在させることはできません。インストールコマンドを実行直後のプロジェクトを確認すると、src/integrations/react/という新しいフォルダがあるのがわかるでしょう。Reactコンポーネントはそこに配置することをお勧めします。

2. Reactアイランドのハイドレーション

上記の例では、サーバー上で静的なReactコンテンツをSSRする方法を示しています。利点は、そのコンポーネントがブラウザで再レンダリングされることがなく、したがって、そのコードがクライアントにダウンロードされることもないことです。しかし、コンポーネントがインタラクティブである必要があり、したがってブラウザでその動作をダウンロードする必要がある場合はどうでしょうか? まず、Reactでシンプルなカウンターの例を作成することから始めましょう。

react.tsxindex.tsx
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { useState } from 'react';
 
// Create React component standard way
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button className="react" onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
 
// Convert React component to Qwik component
export const QCounter = qwikify$(Counter);

Countボタンをクリックしても何も起こらないことに注目してください。これは、Reactがダウンロードされておらず、したがってコンポーネントがハイドレートされていないためです。ReactコンポーネントをダウンロードしてハイドレートするようにQwikに指示する必要がありますが、そのための条件を指定する必要があります。それを早急に行うと、Reactアプリケーションをアイランドに変えることの利点がすべて失われます。この場合、ユーザーがボタンにカーソルを合わせたときにコンポーネントをダウンロードしたいので、qwikify$(){: eagerness: 'hover' }を追加します。

react.tsxindex.tsx
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { useState } from 'react';
 
// Create React component standard way
function Counter() {
  // Print to console to show when the component is rendered.
  console.log('React <Counter/> Render');
  const [count, setCount] = useState(0);
  return (
    <button className="react" onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
 
// Specify eagerness to hydrate component on hover event.
export const QCounter = qwikify$(Counter, { eagerness: 'hover' });

この例では、舞台裏で何が起こっているかを示すためにコンソールを立ち上げました。ボタンにカーソルを合わせると、Reactコンポーネントがレンダリングされるのがわかります。これは、hover時にコンポーネントをハイドレートするようにQwikに指示したためです。コンポーネントがハイドレートされたので、操作するとカウントが正しく更新されます。

qwikify$()eagernessプロパティを指定することで、コンポーネントがハイドレートされる条件を微調整できるようになり、アプリケーションの起動パフォーマンスに影響を与えます。

3. アイランド間の通信

前の例では、遅延ハイドレートした単一のアイランドがありました。しかし、複数のアイランドがある場合は、それらの間で通信する必要が出てきます。この例では、Qwikでアイランド間の通信を行う方法を示します。

react.tsxindex.tsx
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
 
function Button({ onClick }: { onClick: () => void }) {
  console.log('React <Button/> Render');
  return <button onClick={onClick}>+1</button>;
}
 
function Display({ count }: { count: number }) {
  console.log('React <Display count=' + count + '/> Render');
  return <p className="react">Count: {count}</p>;
}
 
export const QButton = qwikify$(Button, { eagerness: 'hover' });
export const QDisplay = qwikify$(Display);

上記の例では、ボタン用(QButton)と表示用(QDisplay)の2つのアイランドがあります。ボタンアイランドはhover時にハイドレートされ、表示アイランドはいかなるイベントでもハイドレートされません。

react.tsxファイルには次のものがあります。

  • QButton - クリックするとカウントを増やすボタン。このアイランドはhover時にハイドレートされます。
  • QDisplay - 現在のカウントを表示するディスプレイ。このアイランドはいかなるイベントでもハイドレートされませんが、そのpropsが変更されるとQwikによってハイドレートされます。
  • 両方のReactコンポーネントには、コンポーネントがハイドレートまたは再レンダリングされたときに表示するconsole.logがあります。

index.tsxファイルには次のものがあります。

  • count - 現在のカウントを追跡するために使用されるシグナル。
  • QButtonアイランドをインスタンス化します。onClick$ハンドラーはcountシグナルを増分します。QwikはonClickonClick$プロップに自動的に変換し、イベントハンドラーの遅延読み込みを可能にすることに注意してください。
  • QDisplayアイランドをインスタンス化します。countシグナルはpropとしてアイランドに渡されます。

ボタンにカーソルを合わせると、QButtonアイランドがハイドレートされるのがわかります。ボタンをクリックすると、QDisplayアイランドがハイドレートされ、カウントが更新されるのがわかります。(QDisplayの二重実行は、最初に初期ハイドレーションが行われ、次にカウントが更新されるためです。)

Qwik Reactは、インタラクティビティを持つコンポーネントのみを早急にハイドレートする必要があることに注意してください。動的ではあるが、インタラクティビティを持たないコンポーネント(この例のQDisplayなど)は、早急にハイドレートする必要はなく、代わりにpropsが変更されると自動的にハイドレートされます。

また、ブラウザでconsole.log('Qwik Render');が実行されないことにも注意してください。

4. host:リスナー

前の例では、2つのアイランドがありました。QButtonは、ReactがonClickイベントハンドラーを設定できるように、早急にハイドレートする必要がありました。これは、QButtonアイランドの出力が静的であるため、再レンダリングする必要がないため、少し無駄です。QButtonをクリックしても、QButtonアイランドは再レンダリングされません。そのような場合、Qwikに、リスナーをアタッチするためだけにReactでコンポーネント全体をハイドレートするのではなく、clickリスナーを登録するように依頼できます。これは、イベント名にhost:プレフィックスを使用することで行われます。

index.tsxreact.tsx
import { component$, useSignal } from '@builder.io/qwik';
import { QButton, QDisplay } from './react';
 
export default component$(() => {
  console.log('Qwik Render');
  const count = useSignal(0);
  return (
    <main>
      <QButton
        host:onClick$={() => {
          console.log('click', count.value);
          count.value++;
        }}
      >
        +1
      </QButton>
      <QDisplay count={count.value}></QDisplay>
    </main>
  );
});

ボタンにカーソルを合わせても何も起こりません(Reactハイドレーションなし)。ボタンをクリックすると、Qwikがイベントを処理してシグナルを更新し、その結果QDisplayアイランドがハイドレートされます。QButtonアイランドは決してハイドレートされないことに注意してください。したがって、この変更により、サーバー側のみでレンダリングされ、ブラウザでハイドレートする必要がないアイランドを持つことができ、ユーザーの時間を節約できます。

5. childrenの投影

一般的なユースケースは、コンテンツの子をコンポーネントに渡すことです。これはQwik Reactでも同様に機能します。Reactコンポーネントでは、propsにchildrenを宣言し、予想どおりに使用するだけです(react.tsxを参照)。

index.tsxreact.tsx
import { component$, useSignal } from '@builder.io/qwik';
import { QFrame } from './react';
 
export default component$(() => {
  console.log('Qwik Render');
  const count = useSignal(0);
  return (
    <QFrame>
      <button
        onClick$={() => {
          console.log('click', count.value);
          count.value++;
        }}
      >
        +1
      </button>
      Count: {count}
    </QFrame>
  );
});

QFrameアイランドには、ハイドレーションをトリガーするeagernessまたはpropsがないため、決してハイドレートされないことに注意してください。しかし、シグナルが変化すると子が再レンダリングされ、アイランドがハイドレートされなくても、React QFrameアイランドに正しく投影されることにも注意してください。これにより、ページをさらにサーバー側でレンダリングし、クライアントでレンダリングしないようにすることができます。

6. Reactライブラリの使用

最後に、QwikアプリケーションでReactライブラリを使用することができます。この例では、Material UIEmotionを使用して、このReactの例をレンダリングしています。

react.tsxindex.tsx
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
import { type ReactNode } from 'react';
 
export const Example = qwikify$(
  function Example({
    selected,
    onSelected,
    children,
  }: {
    selected: number;
    onSelected: (v: number) => any;
    children?: ReactNode[];
  }) {
    console.log('React <Example/> Render');
    return (
      <>
        <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
          <Tabs
            value={selected}
            onChange={(e, v) => onSelected(v)}
            aria-label="basic tabs example"
          >
            <Tab label="Item One" />
            <Tab label="Item Two" />
            <Tab label="Item Three" />
          </Tabs>
          {children}
        </Box>
      </>
    );
  },
  { eagerness: 'hover' }
);

Reactの例はカーソルを合わせたときにハイドレートされ、期待どおりに機能します。

ルール

Qwik Reactのルールをより深く理解するために、この例を見てみましょう。

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' });

重要:ファイルの先頭に/** @jsxImportSource react */をインポートする必要があります。これは、コンパイラーにReactをJSXファクトリとして使用するように指示するものです。

要するに、ルールは次のとおりです。

  1. ReactとQwikのコンポーネントを同じファイル内で混在させないでください。
  2. すべてのReactコードをsrc/integrations/reactフォルダー内に配置することをお勧めします。
  3. Reactコードを含むファイルの先頭に/** @jsxImportSource react */を追加します。
  4. qwikify$()を使用して、ReactコンポーネントをQwikコンポーネントに変換し、Qwikモジュールからインポートできるようにします。

これで、QwikはMUIButtonをインポートして、他のQwikコンポーネントと同様に使用できるようになりました。

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$()

qwikify$(ReactCmp, options?): QwikCmpを使用すると、Reactコンポーネントの部分的なハイドレーションを実装できます。これは、ReactのSSRおよびハイドレーションロジックを、SSR中にReactのrenderToString()を実行し、指定された場合にhydrateRoot()を動的に呼び出すことができるQwikコンポーネントにラップすることで機能します。

デフォルトではブラウザでReactコードは実行されないことに注意してください。つまり、Reactコンポーネントはデフォルトではインタラクティブではありません。たとえば、次の例では、MUIのSliderコンポーネントをqwikifyしますが、インタラクティブではありません(Reactコンポーネントをブラウザでハイドレートする時期をQwikに指示するeagernessプロパティがありません)。

react.tsxindex.tsx
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { Slider } from '@mui/material';
export const MUISlider = qwikify$<typeof Slider>(
  Slider
  //  Uncomment next line to make component interactive in browser
  // { eagerness: 'hover' }
);

制限事項

すべてのqwikified Reactコンポーネントは分離されています

qwikified Reactコンポーネントの各インスタンスは、独立したReactアプリになります。完全に隔離されています。

export const MUISlider = qwikify$(Slider);
 
<MUISlider></MUISlider>
<MUISlider></MUISlider>
  • MUISliderは、独自のステート、ライフサイクルなどを持つ完全に隔離されたReactアプリケーションです。
  • スタイルは重複します
  • 状態は共有されません
  • コンテキストは継承されません。
  • アイランドは独立してハイドレートします。

デフォルトではインタラクティビティは無効になっています

デフォルトでは、qwikifiedコンポーネントはインタラクティブになりません。インタラクティビティを有効にする方法については、次のセクションを参照してください。

qwikify$()を移行戦略として使用する

QwikでReactコンポーネントを使用することは、アプリケーションをQwikに移行するための優れた方法ですが、万能薬ではありません。Qwikの機能を活用するために、コンポーネントを書き換える必要があります。

threejsdata-grid libsなど、Reactエコシステムを楽しむための優れた方法でもあります。

アプリケーションを構築するためにqwikify$()を乱用しないでください。乱用するとパフォーマンス上の利点が失われます。

リーフノードではなく、ワイドアイランドを構築する

たとえば、リストを作成するために複数のMUIコンポーネントを使用する必要がある場合は、個々のMUIコンポーネントをqwikifyするのではなく、リスト全体を単一のqwikified Reactコンポーネントとして構築します。

GOOD:ワイドアイランド

すべてのMUIコンポーネントを含む単一のqwikifiedコンポーネント。スタイルは重複せず、コンテキストとテーマ設定は期待どおりに機能します。

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:リーフノード

リーフノードは独立してqwikifiedされます。これにより、多数のネストされたReactアプリケーションがレンダリングされ、それぞれが他のアプリケーションから完全に分離され、スタイルが重複します。

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>
  );
});

インタラクティビティの追加

インタラクティビティを追加するには、React用語で言うと、ハイドレートする必要があります。通常、Reactアプリケーションでは、このハイドレーションタスクはロード時に無条件に実行され、大規模なオーバーヘッドを追加し、サイトを遅くします。

Qwikを使用すると、client: JSXプロパティを使用して、コンポーネントをいつハイドレートするかを決定できます。この手法は、Astroによって普及した部分的なハイドレーションと呼ばれます。

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

Qwikには、すぐに使用できるさまざまな戦略が付属しています

client:load

ドキュメントがロードされると、コンポーネントはすぐにハイドレートされます。

<MUISlider client:load></MUISlider>

ユースケース: 可及的速やかにインタラクティブにする必要のある、すぐに表示されるUI要素。

client:idle

ブラウザが最初にアイドル状態になったとき、つまり、重要な処理がすべて実行された後に、コンポーネントはすぐにハイドレートされます。

<MUISlider client:idle></MUISlider>

ユースケース: すぐにインタラクティブにする必要のない、優先度の低いUI要素。

client:visible

コンポーネントがビューポートに表示されたときに、すぐにハイドレートされます。

<MUISlider client:visible></MUISlider>

ユースケース: ページの下部(「スクロールで見えない部分」)にあるか、読み込みにリソースを非常に消費するため、ユーザーが要素を見ない場合はまったく読み込まないようにしたい、優先度の低いUI要素。

client:hover

マウスがコンポーネント上にあるときに、コンポーネントはすぐにハイドレートされます。

<MUISlider client:hover></MUISlider>

ユースケース: インタラクティブ性が重要ではなく、デスクトップでのみ実行する必要がある、最も優先度の低いUI要素。

client:signal

これは、渡されたシグナルがtrueになったときにいつでもコンポーネントをハイドレートできる高度なAPIです。

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

これにより、ハイドレーションのカスタム戦略を効果的に実装できます。

client:event

指定されたDOMイベントがディスパッチされたときに、コンポーネントはすぐにハイドレートされます。

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

client:only

trueの場合、コンポーネントはSSRでは実行されず、ブラウザでのみ実行されます。

<MUISlider client:only></MUISlider>

Reactイベントのリスニング

Reactのイベントは、関数をコンポーネントのプロパティとして渡すことによって処理されます。例:

// React code (won't work in Qwik)
 
import { Slider } from '@mui/material';
 
<Slider onChange={() => console.log('value changed')}></Slider>;

qwikify()関数は、これをQwikコンポーネントに変換し、ReactイベントをQwikのQRLとしても公開します。

import { Slider } from '@mui/material';
import { qwikify$ } from '@builder.io/qwik-react';
const MUISlider = qwikify$(Slider);
 
<MUISlider client:visible onChange$={() => console.log('value changed')} />;

コンポーネントをすぐにハイドレートするためにclient:visibleプロパティを使用していることに注意してください。そうしないと、コンポーネントがインタラクティブにならず、イベントがディスパッチされることはありません。

ホスト要素

Reactコンポーネントをqwikify$()でラップすると、内部的には、次のような新しいDOM要素が作成されます。

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

ラッパー要素のタグ名はtagNameで構成可能であることに注意してください。例:qwikify$(ReactCmp, { tagName: 'my-react' })

ハイドレーションなしでDOMイベントをリッスンする

ホスト要素はReactの一部ではないため、イベントをリッスンするためにハイドレーションは必要ありません。カスタム属性とイベントをホスト要素に追加するには、JSXプロパティでhost:プレフィックスを使用できます。例:

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

これにより、Reactコードを1バイトもダウンロードせずに、MUIボタンのクリックに応答することを効果的に可能にします。

🧑‍💻ハッピーハッキング!

貢献者

このドキュメントの改善に協力してくれたすべての貢献者に感謝します!

  • manucorporat
  • swwind
  • reemardelarosa
  • mhevery
  • AnthonyPAlicea
  • adamdbradley
  • igorbabko
  • abhi-works
  • Benny-Nottonson
  • mrhoodz