Qwik React ⚛️

Qwik React 允許你在 React 中使用 Qwik。 使用 Qwik React 的好處是,你可以在 Qwik 中使用現有的 React 組件和函式庫。 這讓你能夠利用 React 組件和函式庫的大型生態系統,例如 Material UIThreejsReact Spring。 這也是一種在不重寫 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. 配置 Vite 以使用 @builder.io/qwik-react 插件
    // 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. 你好,世界

讓我們從一個簡單的例子開始。 我們將建立一個簡單的 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 套件匯出了 qwikify$() 函式,它可以讓你將 React 組件轉換為 Qwik 組件,你可以在整個應用程式中使用這些組件。

**注意:**你不能在 Qwik 中使用 React 組件,而不先使用 qwikify$() 將其轉換。 儘管 React 和 Qwik 組件看起來很相似,但它們在根本上是非常不同的。

React 和 Qwik 組件不能混合在同一個檔案中,如果你在執行安裝命令後立即檢查你的專案,你會看到一個新的資料夾 src/integrations/react/,我們建議你將你的 React 組件放在那裡。

2. 水合 react 島嶼

上面的例子展示了如何在伺服器上 SSR 靜態 React 內容。 好處是,該組件永遠不會在瀏覽器中重新渲染,因此其代碼永遠不會下載到客戶端。 但是,如果組件需要是交互式的,因此我們需要在瀏覽器中下載其行為,該怎麼辦? 讓我們從在 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 尚未被下載,因此組件沒有被激活。我們需要告訴 Qwik 下載 React 組件並激活它,但我們需要指定在哪種情況下我們想要這樣做。急切地執行會失去將 React 應用程序轉換為 islands 的所有好處。在這種情況下,我們希望在用戶將鼠標懸停在按鈕上時下載組件,我們通過將 {: eagerness: 'hover' } 添加到 qwikify$() 來做到這一點。

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 組件被渲染。這是因為我們要求 Qwik 在 hover 上激活組件。現在組件已被激活,您可以與之交互,它將正確更新計數。

通過為 qwikify$() 提供 eagerness 屬性,我們允許您微調組件被激活的條件,這將影響應用程序的啟動性能。

3. 島嶼間通訊

在前面的示例中,我們有一個單獨的、延遲激活的 island。但是,一旦您有多個 island,就需要在它們之間進行通信。此示例展示了如何使用 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);

在上面的示例中,我們有兩個 island,一個用於按鈕 (QButton),另一個用於顯示 (QDisplay)。按鈕 island 在 hover 上被激活,而顯示 island 在任何事件上都沒有被激活。

react.tsx 文件包含

  • QButton - 一個在點擊時增加計數的按鈕。這個 island 在 hover 上被激活。
  • QDisplay - 顯示當前計數的顯示器。這個 island 在任何事件上都沒有被激活,但當其 props 改變時,Qwik 會激活它。
  • 兩個 React 組件都有 console.log 來顯示組件何時被激活或重新渲染。

index.tsx 文件包含

  • count - 一個用於跟踪當前計數的信號。
  • 實例化 QButton island。onClick$ 處理程序增加 count 信號。請注意,Qwik 會自動將 onClick 轉換為 onClick$ prop,允許延遲加載事件處理程序。
  • 實例化 QDisplay island。count 信號作為 prop 傳遞給 island。

當您將鼠標懸停在按鈕上時,您將看到 QButton island 被激活。當您點擊按鈕時,您將看到 QDisplay island 被激活並且計數被更新。(QDisplay 的兩次執行是由於第一次是初始激活,第二次是更新計數。)

請注意,Qwik React 只需要急切地激活具有交互性的組件。動態的但沒有交互性的組件(例如本例中的 QDisplay)不需要急切地被激活,而是在其 props 改變時自動被激活。

還要注意到,console.log('Qwik Render'); 永遠不會在瀏覽器中執行。

4. host: 監聽器

在前面的例子中,我們有兩個 islands。 QButton 必須要先進行 hydration,這樣 React 才能設置 onClick 事件處理程序。 這有點浪費,因為 QButton island 的輸出是靜態的,所以它永遠不需要被重新渲染。 點擊 QButton 不會導致 QButton island 重新渲染。 在這種情況下,我們可以要求 Qwik 註冊 click 監聽器,而不是為了附加一個監聽器而在 React 中對整個組件進行 hydration。 這是通過在事件名稱中使用 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 hydration)。 點擊按鈕將導致 Qwik 處理事件並更新信號,進而導致 QDisplay island 的 hydration。 請注意,QButton island 永遠不會被 hydration。 因此,這一變化使我們能夠擁有一個只在服務器端渲染且永遠不需要在瀏覽器中 hydration 的 island,從而節省了用户的時間。

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 island 永遠不會被 hydration,因為它沒有 eagerness 或任何會觸發 hydration 的 props。 但同時也要注意,當信號發生變化時,子組件會重新渲染,並正確地投影到 React QFrame island 中,而不會對 island 進行 hydration。 這使得頁面的更多部分可以在服務器端渲染,並且永遠不需要在客戶端渲染。

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 示例在鼠標懸停時進行 hydration,並按預期工作。

規則

讓我們看一下這個例子,以更好地理解 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 和水合邏輯封裝到一個 Qwik 組件中,該組件可以在 SSR 期間執行 React 的 renderToString() 並在指定時動態調用 hydrateRoot()

請注意,默認情況下不會在瀏覽器中運行任何 React 代碼,這意味著 React 組件默認情況下不會是交互式的,例如,在以下示例中,我們將 MUI 中的 Slider 組件 qwikify,但它不會是交互式的(它缺少一個 eagerness 屬性來告訴 Qwik 何時應該在瀏覽器中水合 React 組件。)

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 應用程序,具有自己的狀態、生命週期等。
  • 樣式將被複製
  • 狀態不會被共享
  • Context 不會被繼承。
  • Islands 將獨立地 水合

默認情況下禁用交互性

默認情況下,qwikified 組件將不會是交互式的,請查看下一節以了解如何啟用交互性。

使用 qwikify$() 作為遷移策略

在 Qwik 中使用 React 組件是將應用程序遷移到 Qwik 的一種很好的方法,但它不是萬能的,您需要重寫組件以利用 Qwik 的功能。

它也是享受 React 生態系統的一種很好的方法,例如 threejsdata-grid libs

不要濫用 qwikify$() 來構建您的應用程序,因為過度使用會導致性能提升的損失。

構建寬島,而不是葉節點

例如,如果您需要使用多個 MUI 組件來構建一個列表,請勿 qwikify 每個單獨的 MUI 組件,而是將整個列表構建為一個單獨的 qwikified React 組件。

良好:寬島

一個單獨的 qwikified 組件,其中包含所有 MUI 組件。樣式不會被複製,並且上下文和主題將按預期工作。

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

不良:葉節點

葉節點是獨立 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

這是一個進階 API,允許在傳遞的信號變為 true 時注水元件。

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 屬性來立即注水元件,否則元件將無法互動,事件也永遠不會被觸發。

宿主元素

使用 qwikify$() 包裝 React 元件時,會在底層建立一個新的 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 程式碼的情況下,回應 MUI 按鈕 中的點擊事件。

🧑‍💻祝您開發愉快!

貢獻者

感謝所有幫助改進此文件的貢獻者!

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