Qwik React ⚛️
Qwik React 允许您在 Qwik 中使用 React。使用 Qwik React 的优势是您可以在 Qwik 中使用现有的 React 组件和库。这使您可以利用 React 组件和库的大型生态系统,例如 Material UI、Threejs 和 React 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
上述命令将执行以下操作:
- 在
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 库。
- 配置 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 组件。
/** @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 内容。好处是该组件永远不会在浏览器中重新渲染,因此其代码永远不会下载到客户端。但是,如果组件需要交互性,因此我们需要在浏览器中下载其行为时怎么办?让我们从构建一个简单的计数器示例开始。
/** @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' }
来实现。
/** @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 中进行岛屿间通信。
/** @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');
在浏览器中永远不会执行。
host:
事件监听器
4. 在前面的示例中,我们有两个岛屿。QButton
必须急切水合,以便 React 可以设置 onClick
事件处理程序。这有点浪费,因为 QButton
岛屿永远不需要重新渲染,因为其输出是静态的。单击 QButton
不会导致 QButton
岛屿重新渲染。在这种情况下,我们可以要求 Qwik 注册 click
事件监听器,而不是急切水合整个组件以附加监听器。使用 host:
前缀在事件名称中实现此目的。
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
岛屿永远不会被水合。因此,此更改允许我们在服务器端仅呈现岛屿,并且在浏览器中永远不需要水合,从而节省了用户的时间。
children
5. 投影 常见的用例是将内容子项传递给组件。在 Qwik React 中,这也适用。只需在 props 中声明 children
并按预期使用它们(请参阅 react.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 UI 和 Emotion 渲染了此 React 示例。
/** @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 的规则。
/** @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 工厂。
简而言之,规则如下:
- 不要在同一个文件中混合使用 React 和 Qwik 组件。
- 我们建议将所有 React 代码放在
src/integrations/react
文件夹中。 - 在包含 React 代码的文件顶部添加
/** @jsxImportSource react */
。 - 使用
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 组件)。
/** @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 生态系统的好方法,例如 threejs 或 data-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 的点击事件。
🧑💻祝您编程愉快!