Qwik React ⚛️
Qwik React 允许您在 React 中使用 Qwik。使用 Qwik React 的优势在于,您可以在 Qwik 中使用现有的 React 组件和库。这使您可以利用 React 组件和库的庞大生态系统,例如 Material UI、Threejs 和 React 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
上面的命令将执行以下操作
- 在
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: [ ..., // 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 路由中。
/** @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 中构建一个简单的计数器示例开始。
/** @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$()
来实现这一点。
/** @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 进行岛屿间通信。
/** @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');
在浏览器中从未执行。
host:
监听器
4. 在前面的示例中,我们有两个岛屿。QButton
必须急切地水合,以便 React 可以设置 onClick
事件处理程序。这有点浪费,因为 QButton
岛屿永远不需要重新渲染,因为它的输出是静态的。点击 QButton
不会导致 QButton
岛屿重新渲染。在这种情况下,我们可以要求 Qwik 注册 click
监听器,而不是仅仅为了附加监听器而在 React 中水合整个组件。这可以通过在事件名称中使用 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 中也适用。在 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 组件进行了 qwikify 处理,但它不会具有交互性(它缺少 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 组件的实例都成为一个独立的 React 应用程序。完全隔离。
export const MUISlider = qwikify$(Slider);
<MUISlider></MUISlider>
<MUISlider></MUISlider>
默认情况下,交互性被禁用
默认情况下,qwikified 组件不会具有交互性,请查看下一节以了解如何启用交互性。
qwikify$()
作为迁移策略
使用 在 Qwik 中使用 React 组件是将您的应用程序迁移到 Qwik 的一种好方法,但它不是万能药,您需要重写您的组件以利用 Qwik 的功能。
这也是享受 React 生态系统的好方法,例如 threejs 或 data-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 按钮 的点击事件。
🧑💻快乐编程!