Qwik React ⚛️

Qwik React 允许您在 Qwik 中使用 React。使用 Qwik React 的优势是您可以在 Qwik 中使用现有的 React 组件和库。这使您可以利用 React 组件和库的大型生态系统,例如 Material UIThreejsReact Spring。这也是一种在不必重写 React 应用程序的情况下获得 Qwik 的好处的方法。

基本用法

Qwik React 的基本用法是将现有的 React 组件包装在 qwikify$ 函数中。这个函数将创建一个 Qwik 组件,可以在 Qwik 中使用,并将 React 组件转换为一个岛屿,使您可以自由地微调 React 组件何时进行水合。

基本用法

// 这个 pragma 是必需的,以便使用 React JSX 而不是 Qwik JSX
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
 
// 一个现有的 React 组件
function Greetings() {
  return <div>Hello from React</div>;
}
 
// 包装 React 组件的 Qwik 组件
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: [
           ..., 
           // 这是重要的部分
           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';
 
// 创建标准的 React 组件
function Greetings() {
  return <p>Hello from React</p>;
}
 
// 将 React 组件转换为 Qwik 组件
export const QGreetings = qwikify$(Greetings);

@builder.io/qwik-react 包导出了 qwikify$() 函数,该函数允许您将 React 组件转换为 Qwik 组件,您可以在应用程序中使用这些组件。

**注意:**在将 React 组件用于 Qwik 之前,必须先将其转换为 Qwik 组件,使用 qwikify$()。尽管 React 和 Qwik 组件看起来相似,但它们在本质上是非常不同的。

React 和 Qwik 组件不能在同一个文件中混合使用。如果在运行安装命令后立即检查项目,您将看到一个名为 src/integrations/react/ 的新文件夹,我们建议您将 React 组件放在其中。

2. 水合 React 岛屿

上面的示例展示了如何在服务器端静态渲染 React 内容。好处是该组件永远不会在浏览器中重新渲染,因此其代码永远不会下载到客户端。但是,如果组件需要交互性,因此我们需要在浏览器中下载其行为时怎么办?让我们从构建一个简单的计数器示例开始。

react.tsxindex.tsx
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { useState } from 'react';
 
// 创建标准的 React 组件
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button className="react" onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
 
// 将 React 组件转换为 Qwik 组件
export const QCounter = qwikify$(Counter);

请注意,单击 Count 按钮不会发生任何操作。这是因为 React 尚未下载,因此组件未被水合。我们需要告诉 Qwik 下载 React 组件并进行水合,但我们需要指定何时执行该操作。急切地执行此操作将丧失将 React 应用程序转换为岛屿的所有优势。在这种情况下,我们希望在用户悬停在按钮上时下载组件,我们可以通过在 qwikify$() 中添加 {: eagerness: 'hover' } 来实现。

react.tsxindex.tsx
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { useState } from 'react';
 
// 创建标准的 React 组件
function Counter() {
  // 打印到控制台以显示组件何时被渲染。
  console.log('React <Counter/> Render');
  const [count, setCount] = useState(0);
  return (
    <button className="react" onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
 
// 指定渴望度以在悬停事件上水合组件。
export const QCounter = qwikify$(Counter, { eagerness: 'hover' });

在此示例中,我们打开了控制台以显示背后的操作。当您将鼠标悬停在按钮上时,您将看到 React 组件被渲染。这是因为我们要求 Qwik 在 hover 事件上水合组件。现在,组件已经水合,您可以与之交互,并且计数将正确更新。

通过在 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)。按钮岛屿在 hover 上水合,显示岛屿在任何事件上都不水合。

react.tsx 文件包含以下内容:

  • QButton - 一个按钮,点击时会增加计数。此岛屿在 hover 上水合。
  • QDisplay - 一个显示当前计数的组件。此岛屿在任何事件上都不水合,但在其 props 更改时会由 Qwik 进行水合。
  • 两个 React 组件都有 console.log,以显示组件何时被水合或重新渲染。

index.tsx 文件包含以下内容:

  • count - 一个信号,用于跟踪当前计数。
  • 实例化 QButton 岛屿。onClick$ 处理程序递增 count 信号。请注意,Qwik 会自动将 onClick 转换为 onClick$ 属性,从而实现事件处理程序的延迟加载。
  • 实例化 QDisplay 岛屿。将 count 信号作为 prop 传递给岛屿。

当您将鼠标悬停在按钮上时,您将看到 QButton 岛屿被水合。当您点击按钮时,您将看到 QDisplay 岛屿被水合,并且计数被更新。(QDisplay 的双重执行是由于首先进行初始水合,然后更新计数。)

请注意,Qwik React 只需要急切水合具有交互性的组件。具有动态性但没有交互性的组件(例如本示例中的 QDisplay)不需要急切水合,而是在其 props 更改时自动水合。

还请注意,console.log('Qwik Render'); 在浏览器中永远不会执行。

4. host: 事件监听器

在前面的示例中,我们有两个岛屿。QButton 必须急切水合,以便 React 可以设置 onClick 事件处理程序。这有点浪费,因为 QButton 岛屿永远不需要重新渲染,因为其输出是静态的。单击 QButton 不会导致 QButton 岛屿重新渲染。在这种情况下,我们可以要求 Qwik 注册 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 中,这也适用。只需在 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 和水合逻辑包装在一个 Qwik 组件中来工作,该组件可以在 SSR 期间执行 React 的 renderToString(),并在指定时动态调用 hydrateRoot()

请注意,默认情况下,浏览器中不会运行任何 React 代码,这意味着 React 组件默认情况下不会是交互式的。例如,在以下示例中,我们将 MUI 的 Slider 组件转换为 Qwik 组件,但它不会是交互式的(缺少一个 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 应用程序。完全隔离。

export const MUISlider = qwikify$(Slider);
 
<MUISlider></MUISlider>
<MUISlider></MUISlider>
  • 每个 MUISlider 都是一个完全隔离的 React 应用程序,具有自己的状态、生命周期等。
  • 样式将被复制
  • 状态不会共享
  • 上下文 不会被继承。
  • 岛屿将独立水合

默认情况下禁用交互性

默认情况下,qwikified 组件不会是交互式的,请查看下一节以了解如何启用交互性。

使用 qwikify$() 作为迁移策略

在 Qwik 中使用 React 组件是将应用程序迁移到 Qwik 的好方法,但它不是万能的,您需要重写组件以利用 Qwik 的功能。

这也是使用 React 生态系统的好方法,例如 threejsdata-grid libs

不要滥用 qwikify$() 来构建应用程序,过度使用会导致性能下降。

构建宽岛屿,而不是叶节点

例如,如果您需要使用多个 MUI 组件构建列表,请不要将每个单独的 MUI 组件 qwikify,而是将整个列表作为单个 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';
 
// 将整个列表作为单个 qwikified 组件
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>
  );
});

不好:叶节点

叶节点独立地 qwikify,实际上渲染了数十个嵌套的 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);
// 使用数十个嵌套的 React 岛屿的 Qwik 组件
// 每个 MUI-* 都是一个独立的 React 应用程序
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 代码(在 Qwik 中不起作用)
 
import { Slider } from '@mui/material';
 
<Slider onChange={() => console.log('value changed')}></Slider>;

qwikify() 函数将把此代码转换为一个 Qwik 组件,该组件还将将 React 事件公开为 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')} />;

请注意,我们使用 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 button 的点击事件。

🧑‍💻祝您编程愉快!

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • manucorporat
  • swwind
  • reemardelarosa
  • mhevery
  • AnthonyPAlicea
  • adamdbradley