Компоненты

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

Компоненты Qwik уникальны в своём роде:

  • Компоненты Qwik автоматически разбиваются оптимизатором на лениво загружаемые фрагменты;
  • Они являются возобновляемыми (компонент может быть создан на сервере и продолжать выполняться на клиенте);
  • Они являются реактивными и рендерятся независимо от других компонентов на странице. См. описание рендера.

component$()

Компонент Qwik - это функция, которая возвращает JSX, обернутый в вызов component$.

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return <div>Привет, мир!</div>;
});

Причина написания component$ в том, что последующий $ позволяет оптимизатору разбивать компоненты в дереве приложения на отдельные чанки, так что каждый чанк может быть загружен (или не загружен, если он не нужен) независимо. Без знака $ компонент будет загружаться всегда, если необходимо загрузить родительский компонент.

Композиция компонентов

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

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <>
      <p>Родительский текст</p>
      <Child />
    </>
  );
});
 
const Child = component$(() => {
  return <p>Дочерний текст</p>;
});

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

Пример счётчика

Несколько более сложный пример счётчика.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <>
      <p>Счёт: {count.value}</p>
      <button onClick$={() => count.value++}>Прибавить</button>
    </>
  );
});

Параметры

Параметры используются для передачи данных в компонент от родителя. Доступ к параметрам осуществляется через аргумент props функции component$.

В этом примере компонент Item объявляет необязательные параметры name, quantity, description и price.

import { component$ } from '@builder.io/qwik';
 
interface ItemProps {
  name?: string;
  quantity?: number;
  description?: string;
  price?: number;
}
 
export const Item = component$<ItemProps>((props) => {
  return (
    <ul>
      <li>название: {props.name}</li>
      <li>количество: {props.quantity}</li>
      <li>описание: {props.description}</li>
      <li>цена: {props.price}</li>
    </ul>
  );
});
 
export default component$(() => {
  return (
    <>
      <h1>Параметры</h1>
      <Item name="hammer" price={9.99} />
    </>
  );
});

В приведённом выше примере мы используем component$<ItemProps> для предоставления параметру явного типа. Это необязательно, но позволяет компилятору TypeScript проверить правильность использования параметров.

Параметры по умолчанию

Вы можете использовать паттерн деструктуризации с параметрами для предоставления значений по умолчанию.

interface Props {
  enabled?: boolean;
  placeholder?: string;
}
 
// Мы можем использовать деструктуризацию параметров в JS для предоставления значения по умолчанию.
export default component$<Props>(({enabled = true, placeholder = ''}) => {
  return (
    <input disabled={!enabled} placeholder={placeholder} />
  );
});

Реактивный рендер

Компоненты Qwik являются реактивными. Это означает, что они автоматически обновляются при изменении состояния. Существует два вида обновлений:

  1. Состояние привязывается к тексту или атрибуту DOM. Такие изменения обычно напрямую обновляют DOM и не требуют повторного выполнения функции компонента.
  2. Состояние вызывает структурные изменения в DOM (элементы создаются и или удаляются). Такие изменения требуют повторного выполнения функции компонента.

Следует помнить, что при изменении состояния функция вашего компонента может выполняться ноль или более раз в зависимости от того, к чему привязано состояние. По этой причине функция должна быть идемпотентной, и вы не должны полагаться на количество раз ее выполнения.

Изменение состояния приводит к тому, что компонент становится недействительным. Когда компоненты признаются недействительными, они добавляются в очередь обновлений, которая очищается (рендерится) при очередном событии requestAnimationFrame. Очередь служит для объеденения рендера компонентов.

Получение элемента DOM

Используйте ref для получения элемента DOM. Сначала создайте сигнал для хранения элемента DOM и затем передайте его в свойство JSX ref.

Получение ссылок на элементы DOM может быть полезно для вычисления размера элемента (getBoundingClientRect), вычисления стилей, инициализации WebGL, или даже подключения какой-либо сторонней библиотеки, которая взаимодействует с элементами DOM напрямую.

import { component$, useVisibleTask$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const width = useSignal(0);
  const height = useSignal(0);
  const outputRef = useSignal<Element>();
 
  useVisibleTask$(() => {
    if (outputRef.value) {
      const rect = outputRef.value.getBoundingClientRect();
      width.value = Math.round(rect.width);
      height.value = Math.round(rect.height);
    }
  });
 
  return (
    <section>
      <article
        ref={outputRef}
        style={{ border: '1px solid red', width: '100px' }}
      >
        Измените здесь значение текста, чтобы растянуть поле.
      </article>
      <p>
        Красный блок выше имеет высоту {height.value} пикселей и ширину {width.value}{' '}
        пикселей.
      </p>
    </section>
  );
});

Доступ к элементам по id в серверной и клиентской средах

В серверных и клиентских средах к элементам иногда приходится обращаться по их id. Используйте функцию useId() для получения уникального идентификатора текущего компонента, который остаётся неизменным при рендере на стороне сервера (SSR) и при работе на стороне клиента. Это очень важно, когда серверные компоненты требуют выполнения сценариев на стороне клиента, например:

  1. Анимационные эффекты.
  2. Улучшение доступности
  3. Другие клиентские библиотеки.

В системе микрофронтов, где одновременно выполняется несколько фрагментов, useId() обеспечивает уникальность и согласованность идентификаторов во всех средах выполнения, исключая конфликты.

import {
  component$,
  useId,
  useSignal,
  useVisibleTask$,
} from '@builder.io/qwik';
 
export default component$(() => {
  const elemIdSignal = useSignal<string | null>(null);
  const id = useId();
  const elemId = `${id}-example`;
  console.log('server-side id:', elemId);
 
  useVisibleTask$(() => {
    const elem = document.getElementById(elemId);
    elemIdSignal.value = elem?.getAttribute('id') || null;
    console.log('client-side id:', elemIdSignal.value);
  });
 
  return (
    <section>
      <div id={elemId}>
        Этому идентификатору должны соответствовать как консоль на стороне сервера, так и консоль на стороне клиента:
        <br />
        <b>{elemIdSignal.value || null}</b>
      </div>
    </section>
  );
});

Ленивая загрузка

Компонент также играет важную роль в разрыве отношений родитель-потомок в целях разбиения на чанки.

export const Child = () => <span>child</span>;
 
const Parent = () => (
  <section>
    <Child />
  </section>
);

В приведённом выше примере обращение к компоненту Parent подразумевает транзитивную ссылку на компонент Child. Когда упаковщик создаёт чанк, ссылка на Parent также требует упаковки Child (внутри Parent есть зависимость на Child). Эти транзитивные зависимости представляют из себя проблему, так как это означает, что наличие ссылки на корневой компонент приложения будет транзитивно тянуть остальную часть приложения, чего Qwik пытается избежать.

Чтобы избежать описанной выше проблемы, мы не ссылаемся на компоненты напрямую, вместо этого мы ссылаемся на ленивую обёртку. Она создаётся автоматически функцией component$().

import { component$ } from '@builder.io/qwik';
 
export const Child = component$(() => {
  return <p>дочерний</p>;
});
 
export const Parent = component$(() => {
  return (
    <section>
      <Child />
    </section>
  );
});
 
export default Parent;

Для приведённого выше примера оптимизатор выполнит следующее преобразование кода:

const Child = componentQrl(qrl('./chunk-a', 'Child_onMount'));
const Parent = componentQrl(qrl('./chunk-b', 'Parent_onMount'));
const Parent_onMount = () => qrl('./chunk-c', 'Parent_onRender');
const Parent_onRender = () => (
  <section>
    <Child />
  </section>
);

ПРИМЕЧАНИЕ Для простоты показаны не все преобразования. Результирующие символы хранятся для краткости в одном файле.

Обратите внимание, что после того, как оптимизатор преобразовал код, Parent больше не ссылается напрямую на Child. Это важно, потому что позволяет упаковщику свободно перемещать символы в разные бандлы, не увлекая за собой остальную часть приложения.

Что же происходит, когда компоненту Parent нужно отрендерить компонент Child, но компонент Child ещё не загружен? Во-первых, компонент Parent отображает свой DOM следующим образом:

<main>
  <section>
    <!--qv--><!--/qv-->
  </section>
</main>

Как видно из приведённого выше примера, <!--qv--> действует как маркер, куда будет вставлен компонент Child после того, как он будет лениво загружен.

Встроенные компоненты

В дополнение к стандартному component$(), со всеми его свойствами ленивой загрузки, Qwik также поддерживает облегчённые (встроенные) компоненты, которые больше похожи на традиционные фреймворки. Встроенные компоненты также называют инлайновыми компонентами.

import { component$ } from '@builder.io/qwik';
 
// Встроенный компонент: объявляется с помощью стандартной функции.
export const MyButton = (props: { text: string }) => {
  return <button>{props.text}</button>;
};
 
// Компонент: объявляется с помощью `component$()`.
export default component$(() => {
  return (
    <p>
      Некоторый текст:
      <MyButton text="Нажми меня" />
    </p>
  );
});

В приведённом выше примере MyButton является встроенным компонентом. В отличие от стандартного component$(), встроенные компоненты не могут быть лениво загружены индивидуально, они объединяются со своим родительским компонентом. В данном случае:

  • MyButton будет связан с компонентом default;
  • Всякий раз, когда выполняется функция рендера default, она гарантированно вызывает выполнение рендера MyButton;

Можно считать, что они встраиваются в компонент, в котором инстанцируются.

Ограничения

Встроенные компоненты имеют некоторые ограничения, отсутствующие у стандартного component$():

  • Невозможно использовать use*-методы, такие как useSignal или useStore;
  • Невозможно проецировать содержимое через <Slot>.

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

Полиморфные компоненты

Если вам нужно вывести различные типы элемента в зависимости от его свойства, вы можете сделать это следующим образом:

export const Poly = component$(
  <C extends string | FunctionComponent = string | FunctionComponent>({
    as: Cmp = 'div' as C,
    ...props
  }: { as?: C } & PropsOf<string extends C ? 'div' : C>) => {
    return <Cmp {...props}>hi</Cmp>;
  }
);
 
// Все они работают с правильными типами
<>
  <Poly>Hello from a div</Poly>
  <Poly as="a" href="/blog">Blog</Poly>
  <Poly as="input" onInput$={(ev, el) => console.log(el.value)} />
  <Poly as={OtherComponent} someProp />
</>

Обратите внимание на string extends C, это верно только в том случае, если TypeScript не смог вывести тип из реквизита as, и поэтому вы можете указать тип по умолчанию.

Обзор API

Состояние

Стили

  • useStylesScoped$() - добавляет стили к текущему экземпляру компонента
  • useStyles$() - добавляет стили к текущему компоненту

События

  • useOn() - программно добавляет слушателя к текущему компоненту
  • useOnWindow() - программно добавляет слушателя к объекту window
  • useOnDocument() - программно добавляет слушателя к объекту document

Задачи/Жизненный цикл

  • useTask$() - определяет функцию обратного вызова, которая будет вызываться перед рендером и/или при изменении наблюдаемого хранилища;
  • useVisibleTask$() - определяет функцию обратного вызова, которая будет выполняться после рендера, только в браузере.
  • useResource$() - создаёт ресурс для асинхронной загрузки данных;

Другое

Компоненты

  • <Slot> - объявляет слот для отображения контента
  • <SSRStreamBlock> - объявляет блок потока
  • <SSRStream> - объявляет поток
  • <Fragment> - объявляет JSX-фрагмент

См. также

Участники

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

  • RATIU5
  • leifermendez
  • manucorporat
  • adamdbradley
  • cunzaizhuyi
  • shairez
  • the-r3aper7
  • zanettin
  • Craiqser
  • steve8708
  • mforncro
  • georgeiliadis91
  • leader22
  • almilo
  • estherbrunner
  • kumarasinghe
  • mhevery
  • AnthonyPAlicea
  • khalilou88
  • n8sabes
  • fabian-hiller
  • gioboa
  • mrhoodz
  • eecopa
  • drumnistnakano
  • maiieul
  • wmertens
  • aendel