Qwik React ⚛️

Qwik React позволяет использовать компоненты React в Qwik. Преимущество использования Qwik React заключается в том, что вы можете использовать существующие компоненты и библиотеки React в Qwik. Это позволит вам воспользоваться преимуществами большой экосистемы компонентов и библиотек React, таких как Material UI, Threejs и React Spring. Это также хороший способ получить преимущества Qwik без необходимости переписывать ваше React-приложение.

Основы использования

Основны использования Qwik React в том, чтобы взять существующие компоненты React и обернуть их в функцию qwikify$. Эта функция создаст компонент Qwik, который можно использовать внутри Qwik, и который превратит компонент React в остров, позволяя вам точно настроить, когда компонент React должен гидратироваться.

Основы использования

// Эта прагма необходима для того, чтобы вместо Qwik JSX использовался React JSX
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
 
// Существующий компонент React
function Greetings() {
  return <div>Hello from React</div>;
}
 
// Компонент Qwik, обёртывающий компонент React
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. Здравствуй, мир

Давайте начнём с простого примера. Мы создадим простой компонент 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 без их предварительного преобразования с помощью 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';
 
// Создаем компонент React стандартным способом
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button className="react" onClick={() => setCount(count + 1)}>
      Счёт: {count}
    </button>
  );
}
 
// Преобразование компонента React в компонент Qwik
export const QCounter = qwikify$(Counter);

Обратите внимание, что нажатие на кнопку Count ничего не даёт. Это происходит потому, что React не был загружен, и поэтому компонент не был гидратирован. Нам нужно указать Qwik загрузить компонент React и гидратировать его, но нам нужно указать условие, при котором мы хотим это сделать. Если делать это сразу, то все преимущества превращения приложения React в острова будут потеряны. В данном случае мы хотим загружать компонент, когда пользователь наводит курсор на кнопку, для этого мы добавляем {: eagerness: 'hover' } to qwikify$().

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}
    </button>
  );
}
 
// Укажите готовность к гидратации компонента при событии hover.
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}</p>;
}
 
export const QButton = qwikify$(Button, { eagerness: 'hover' });
export const QDisplay = qwikify$(Display);

В приведённом выше примере у нас есть два острова, один для кнопки (QButton) и один для дисплея (QDisplay). Остров кнопки гидратируется при событии hover, а остров дисплея не гидратируется ни при каких событиях.

Файл react.tsx имеет:

  • QButton - кнопка, при нажатии на которую увеличивается счетчик. Этот остров гидратируется при hover.
  • QDisplay - дисплей, отображающий текущий счетчик. Этот остров не гидратируется ни при каких событиях, но будет гидратирован Qwik, когда изменятся его параметры.
  • Оба React-компонента имеют console.log, чтобы показать, когда компонент гидратирован или перерендерен.

Файл index.tsx имеет:

  • count - сигнал, который используется для отслеживания текущего счёта.
  • Создает остров QButton. Обработчик onClick$ увеличивает сигнал count. Обратите внимание, что Qwik автоматически преобразует onClick в onClick$, позволяя лениво загружать обработчики событий.
  • Инстанцирует острова QDisplay. Сигнал count передаётся острову в качестве параметра.

При наведении курсора на кнопку вы увидите, что остров QButton гидратирован. Когда вы нажмёте на кнопку, вы увидите, что остров QDisplay гидратируется и счётчик обновляется (двойное выполнение QDisplay связано с тем, что сначала происходит первоначальная гидратация, а затем обновление счета).

Обратите внимание, что в Qwik React нужно сразу гидратировать компоненты, обладающие интерактивностью. Компоненты, которые являются динамическими, но не обладают интерактивностью (такие как QDisplay в этом примере), не нуждаются в предварительной гидратации, вместо этого они автоматически гидратируются при изменении их параметров.

Также обратите внимание, что 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 просто объявите 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}
    </QFrame>
  );
});

Заметьте, что остров QFrame никогда не гидратируется, потому что у него нет свойства eagerness или каких-либо других параметров, которые могли бы вызвать гидратацию. Но обратите внимание, что дочерние компоненты действительно ререндерятся при изменении сигнала и правильно проецируются в React-остров QFrame без гидратации острова. Это позволяет ещё большей части страницы быть отрендеренной на стороне сервера и никогда не рендериться на клиенте.

6. Использование React-библиотек

Наконец, в приложении Qwik можно использовать библиотеки React. В этом примере Material UI и Emotion используются для рендера этого примера 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. Добавьте /** @jsxImportSource react */ в начало файлов, содержащих код 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>Привет, это кнопка</MUIButton>
 
      <MUIAlert severity="warning">Это предупреждение от Qwik</MUIAlert>
    </>
  );
});

qwikify$()

Функция qwikify$(ReactCmp, опции?): QwikCmp позволяет реализовать частичную гидратацию компонентов React. Она работает, оборачивая логику SSR и гидратации React в компонент Qwik, который может выполнять метод React renderToString() во время SSR и динамически вызывать hydrateRoot(), когда требуется.

Обратите внимание, что по умолчанию никакой код React не будет выполняться в браузере, то есть компонент React по умолчанию НЕ будет интерактивным. В следующем примере мы квикифицируем компонент Слайдер из библиотеки MUI, но он не будет интерактивным (в нем отсутствует свойство 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
  // Раскомментируйте следующую строку, чтобы сделать компонент интерактивным в браузере
  // { eagerness: 'hover' }
);

Ограничения

Каждый квикифицированный react-компонент изолирован

Каждый экземпляр квикифицированного react-компонента становится независимым приложением React. Полностью изолированным.

export const MUISlider = qwikify$(Slider);
 
<MUISlider></MUISlider>
<MUISlider></MUISlider>
  • Каждый MUISlider - это полностью изолированное React-приложение, со своим состоянием, жизненным циклом и т.д.;
  • Стили будут продублированы;
  • Состояние будет изолировано;
  • Контекст не наследуется;
  • Острова будут гидратироваться независимо.

По умолчанию интерактивность отключена

По умолчанию квикифицированные компоненты не являются интерактивными, обратитесь к следующему разделу, чтобы узнать, как включить интерактивность.

Использование qwikify$() в качестве стратегии миграции

Использование компонентов React в Qwik - это отличный способ перенести ваше приложение на Qwik, но это не серебряная пуля, вам придётся переписать ваши компоненты, чтобы воспользоваться возможностями Qwik.

Это также отличный способ насладиться экосистемой React, например, threejs или data-grid libs.

Не злоупотребляйте qwikify$() для построения приложения, так как чрезмерное использование приведет к потере производительности.

Стройте широкие острова, а не листовые узлы

Например, если вам нужно использовать несколько компонентов MUI для создания списка, не надо квикифицировать каждый компонент MUI по отдельности, вместо этого создайте один квикифицированный React-компонент.

ХОРОШО: Широкий остров

Единственный квикифицированный компонент, внутри которого находятся все компоненты 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';
 
// Квикифицируется весь список
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>
  );
});

ПЛОХО: Листовые узлы

Листовые узлы работают независимо, эффективно создавая десятки вложенных приложений 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, использующий десятки вложенных островков React
// Каждый 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>

Когда использовать: Непосредственно видимые элементы пользовательского интерфейса, которые должны быть интерактивными как можно скорее.

client:idle

Компонент гидратируется в момент, когда браузер начинает простаивать, т.е. когда всё важное уже запущено.

<MUISlider client:idle></MUISlider>

Когда использовать: Менее приоритетные элементы пользовательского интерфейса, которые не нуждаются в немедленном взаимодействии.

client:visible

Компонент гидратируется в момент, когда становится видимым в области просмотра.

<MUISlider client:visible></MUISlider>

Когда использовать: Низкоприоритетные элементы пользовательского интерфейса, которые либо находятся далеко внизу страницы ("за краем"), либо настолько ресурсоемкие для загрузки, что вы предпочтёте не загружать их вообще, если пользователь никогда не увидит этот элемент.

client:hover

Компонент гидратируется в момент наведения на него мыши.

<MUISlider client:hover></MUISlider>

Когда использовать: Низкоприоритетные элементы пользовательского интерфейса, интерактивность которых не имеет решающего значения, и которые работают только на рабочем столе.

client:signal

Это расширенный API, который позволяет гидратировать компонент каждый раз, когда переданный сигнал становится true.

export default component$(() => {
  const hydrateReact = useSignal(false);
  return (
    <>
      <button onClick$={() => (hydrateReact.value = true)}>При клике выполнить гидратацию слайдера</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('значение изменено')}></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('значение изменено')} />;

Обратите внимание, что мы используем свойство client:visible для безотлагательной гидратации компонента, иначе компонент не был бы интерактивным и события никогда бы не диспетчеризировались.

Хост-элемент

При обёртывании компонента React с помощью qwikify$() под капотом создается новый элемент DOM:

<qwik-react>
  <button class="MUI-button"></button>
</qwik-react>

Обратите внимание, что имя тега элемента обёртки настраивается через tagName: qwikify$(ReactCmp, { tagName: 'my-react' }).

Прослушивание событий DOM без гидратации

Хост-элемент не является частью React, и это означает, что для прослушивания событий гидратация не потребуется. Чтобы добавить пользовательские атрибуты и события к элементу-обёртке, вы можете использовать префикс host: в свойствах JSX, например:

<MUIButton
  host:onClick$={() => {
    console.log('Клик по компоненту React без гидратации!');
  }}
/>

Это позволит вам эффективно реагировать на нажатие кнопки MUI button, не загружая ни одного байта React-кода.

🧑‍💻Счастливого кодинга!

Участники

Спасибо всем участникам, которые помогли сделать эту документацию лучше!

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