Рендер

Рендер - это процесс обновления DOM на основе изменений в состоянии приложения и в шаблонах компонентов.

Уникальность Qwik заключается в том, что он умеет рендерить шаблоны вне очередности, асинхронно, и гранулярно.

JSX

Подобно React, Qwik использует JSX для выражения шаблона компонента. Обратите внимание, что JSX - это только синтаксис, под капотом React и Qwik совершенно разные. JSX != VDOM.

Qwik имеет несколько отличий от других JSX-фреймворков:

  • Компоненты всегда объявляются с помощью функции component$;
  • Компоненты могут использовать хук useSignal для создания реактивного состояния;
  • Обработчики событий объявляются с суффиксом $;
  • Для <input> событие onChange в Qwik называется onInput$;
  • Предпочтительны атрибуты HTML. class вместо className. for вместо htmlFor.
import { component$, useSignal } from '@builder.io/qwik';
 
export const MyComponent = component$((props) => {
  const count = useSignal(0);
  return (
    <>
      <button
        onClick$={() => {
          count.value = count.value + props.step;
        }}
      >
        Увеличить на {props.step}
      </button>
      <main
        class={{
          even: count.value % 2 === 0, // условный класс
          odd: count.value % 2 === 1,
        }}
      >
        <h1>Счёт: {count.value}</h1>
      </main>
    </>
  );
});

Рендер дочерних компонентов

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

import { component$, useSignal } from '@builder.io/qwik';
 
export const Parent = component$(() => {
  const count = useSignal(0);
 
  return (
    <>
      <button onClick$={() => (count.value += 1)}>Прибавить</button>
      <Child name={'Мир_' + count.value} />
    </>
  );
});
 
export const Child = component$((props: { name: string }) => {
  return <p>Привет, {props.name}</p>;
});

В приведённом выше примере родительский компонент передает изменяющееся свойство name дочернему компоненту. Компонент Child будет ререндериться только при изменении сигнала count.

Вывод списка элементов

Часто приходится выводить компоненты путём перебора массива в рендер-функции с помощью items.map(). Необходимо, чтобы каждый элемент списка имел уникальное свойство key, передаваемое первому дочернему элементу функции рендера. key должен быть строкой или числом, и должен быть уникальным в пределах списка.

import { component$ } from '@builder.io/qwik';
 
export const Parent = component$(() => {
  return (
    <>
      {data.map(({ message, uniqueKey }) => (
        <div key={uniqueKey}>
          <p>{message}</p>
        </div>
      ))}
    </>
  );
});

Примечание: не рекомендуется использовать индекс массива в качестве ключа, если вы не можете гарантировать, что массив не будет меняться. Всегда предпочтительнее использовать в качестве ключа какой-либо уникальный идентификатор из данных.

Условный рендер

Условный рендер осуществляется с помощью Javascipt-тернарного оператора ?, оператора && или просто с помощью выражения if.

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  const show = useSignal(false);
  return (
    <>
      <button onClick$={() => show.value = !show.value}>Переключить</button>
      {show.value ? <h1>Видимый</h1> : <h1>Скрытый</h1>}
      {show.value && <div>Показывается только при видимом статусе</div>}
    </>
  );
});

dangerouslySetInnerHTML

Qwik предлагает для HTML-элементов атрибут под названием dangerouslySetInnerHTML в качестве замены вызова innerHTML в DOM.

Из-за возможности межсайтового скриптинга (XSS) при рендере недостоверного содержимого, вы должны использовать этот атрибут, чтобы помнить о том, что эта операция может быть опасной.

const htmlString = "<strong>hello</strong>";
<div dangerouslySetInnerHTML={htmlString}></div>

Атрибут bind

Это удобный API для двусторонней привязки данных значения <input> к Signal.

Для чекбоксов можно использовать bind:checked, который связывает булево значение checked с указанным сигналом.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const firstName = useSignal('');
  const acceptConditions = useSignal(false);
 
  return (
    <form>
      <input type="text" bind:value={firstName} />
      <input type="checkbox" bind:checked={acceptConditions} />
 
      <div>Имя: {firstName.value}</div>
    </form>
  );
})

Это не работает с useStore, так как он не возвращает сигнал, но вы всё-равно можете использовать ручной подход (value + onInput$), как показано ниже.

Оптимизатор Qwik компилирует bind: в набор свойств и обработчик событий, т.е. это просто синтаксический сахар.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const firstName = useSignal('');
  const acceptConditions = useSignal(false);
 
  return (
    <form>
      <input type="text"
        value={firstName.value}
        onInput$={(_, el) => firstName.value = el.value }
      />
      <input type="checkbox"
        checked={acceptConditions.value}
        onChange$={(_, el) => acceptConditions.value = el.checked }
      />
      <div>Имя: {firstName.value}</div>
    </form>
  );
})

Этот API гарантирует, что значение элемента всегда синхронизировано со значением сигнала, независимо от того, откуда пришло изменение.

Асинхронный рендеринг

Важно, чтобы конвейер рендера мог лениво загружать дочерние компоненты. Ленивая загрузка - это асинхронная операция; поэтому рендер должен быть асинхронным. На практике это означает, что функция render() возвращает промис.

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

Пакетный рендер

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

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

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

Гранулярная реактивность

Благодаря гранулярной реактивности Qwik, обновляться будут только те компоненты, которые зависят от состояния. Это большой прирост производительности по двум причинам:

  1. Меньше кода для выполнения означает, что обновления приложения будут рендериться быстрее.
  2. Меньше кода для выполнения означает, что код часто не нужно загружать при запуске приложения (или вообще никогда).

Участники

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

  • the-r3aper7
  • RATIU5
  • manucorporat
  • forresst
  • adamdbradley
  • zanettin
  • cunzaizhuyi
  • Pika-Pool
  • Kesmek
  • Craiqser
  • AnthonyPAlicea
  • mhevery
  • igorbabko
  • mrhoodz
  • thejackshelton
  • Balastrong
  • aendel