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. 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 包导出 qwikify$() 函数,该函数允许您将 React 组件转换为 Qwik 组件,您可以在整个应用程序中使用这些组件。

注意:在不先使用 qwikify$() 转换的情况下,您不能在 Qwik 中使用 React 组件。即使 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 应用程序转换为岛屿的所有好处。在这种情况下,我们希望在用户将鼠标悬停在按钮上时下载组件,我们通过将 {: 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 上对组件进行水合。现在组件已进行水合,您可以与它进行交互,它将正确更新计数。

通过将 eagerness 属性赋予 qwikify$(),我们允许您微调组件进行水合的条件,这将影响应用程序的启动性能。

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$ prop,从而允许延迟加载事件处理程序。
  • 实例化 QDisplay 岛屿。count 信号作为 prop 传递给岛屿。

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

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

另外请注意,console.log('Qwik Render'); 在浏览器中从未执行。

4. host: 监听器

在前面的示例中,我们有两个岛屿。QButton 必须急切地水合,以便 React 可以设置 onClick 事件处理程序。这有点浪费,因为 QButton 岛屿永远不需要重新渲染,因为它的输出是静态的。点击 QButton 不会导致 QButton 岛屿重新渲染。在这种情况下,我们可以要求 Qwik 注册 click 监听器,而不是仅仅为了附加监听器而在 React 中水合整个组件。这可以通过在事件名称中使用 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 和水合逻辑包装在一个 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 不会被继承。
  • 岛屿将 水合 独立

默认情况下,交互性被禁用

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

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

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

这也是享受 React 生态系统的好方法,例如 threejsdata-grid 库

不要滥用 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';
 
// 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>
  );
});

不好:叶节点

叶节点是独立地进行 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);
// 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