Задачи

Задачи предназначены для выполнения асинхронных операций в рамках инициализации компонента или изменения его состояния.

Примечание: Задачи похожи на хук useEffect() в React, но различий достаточно, чтобы мы не хотели называть их одинаково, чтобы не привносить заранее сложившиеся ожидания о том, как они работают. Основными отличиями являются:

  • Задачи являются асинхронными;
  • Задача выполняется на сервере и в браузере;
  • Задачи выполняются до рендера и могут блокировать рендер.

useTask$() должен быть вашим стандартным API для запуска асинхронной (или синхронной) работы в рамках инициализации компонента или изменения состояния. Только когда вы не можете достичь желаемого с помощью useTask$(), вы должны рассмотреть возможность использования useVisibleTask$() или useResource$().

Основным вариантом использования useTask$() является выполнение работы по инициализации компонента. useTask$() имеет такие свойства:

  • Он может работать как на сервере, так и в браузере;
  • Он запускается перед рендером и блокирует рендер;
  • Если запущено несколько задач, то они выполняются последовательно в порядке их регистрации. Асинхронная задача будет блокировать выполнение следующей задачи до тех пор, пока она не завершится.

Задачи также можно использовать для выполнения работы при изменении состояния компонента. В этом случае задача будет выполняться заново каждый раз, когда отслеживаемое состояние изменится. Смотри главу track().

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

Иногда задача должна получать данные асинхронно и создавать сигнал (а не блокировать рендер), в этом случае следует использовать useResource$().

Жизненный цикл

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

Примечание: Для систем, использующих гидратацию, выполнение приложения происходит дважды. Один раз на сервере (SSR/SSG) и один раз в браузере (гидратация). По этой причине многие фреймворки имеют "эффекты" которые выполняются только в браузере. Это означает, что код, который выполняется на сервере, отличается от кода, который выполняется в браузере. Выполнение Qwik унифицировано, то есть если код уже был выполнен на сервере, он не будет повторно выполняться в браузере.

В Qwik всего 3 стадии жизненного цикла:

  • Task - запускается перед рендером и при изменении отслеживаемого состояния;
  • Render - запускается после Task и перед VisibleTask;
  • VisibleTask - запускается после Render и когда компонент становится видимым.
      useTask$ -------> RENDER ---> useVisibleTask$
                            |
| --- SERVER or BROWSER --- | ----- BROWSER ----- |
                            |
                       pause|resume

СЕРВЕР: Обычно жизнь компонента начинается на сервере (во время SSR или SSG), в этом случае useTask$ и RENDER будут выполняться на сервере, а затем в браузере запустится VisibleTask после того, как компонент станет видимым.

Заметьте, что поскольку компонент был смонтирован на сервере, в браузере выполняется только useVisibleTask$(). Это происходит потому, что браузер продолжает тот же жизненный цикл, который был приостановлен на сервере после рендера и возобновлён в браузере.

БРАУЗЕР: Иногда компонент первоначально монтируется/рендерится в браузере, например, когда пользователь SPA переходит на новую страницу или когда "модальный" компонент первоначально появляется на странице. В этом случае жизненный цикл будет выглядеть следующим образом:

  useTask$ --> RENDER --> useVisibleTask$
 
| -------------- BROWSER --------------- |

Заметьте, что жизненный цикл точно такой же, но на этот раз все хуки выполняются в браузере, а не на сервере.

useTask$()

  • Когда: ДО первого рендера компонента и при изменении состояния отслеживания
  • Раз: хотя бы раз
  • Платформа: сервер и браузер

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

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

Обратите внимание, что любое последующее выполнение всегда будет происходить в браузере, потому что реактивность - это только для клиента.

                      (state change) -> (re-execute)
                                  ^            |
                                  |            v
 useTask$(track) -> RENDER ->  CLICK  -> useTask$(track)
                        |
  | ----- SERVER ------ | ----------- BROWSER ----------- |
                        |
                   pause|resume

Если useTask$() не отслеживает состояние, она будет запущена только один раз, на сервере или в браузере (но не там и там), в зависимости от того, где первоначально отображается компонент. Эффективно использование в хуке "on mount".

useTask$() будет блокировать рендер компонента до тех пор, пока не разрешится его метод асинхронного обратного вызова, другими словами, задачи выполняются последовательно, даже если они асинхронны (одновременно выполняется только одна задача / задачи блокируют рендер).

Рассмотрим простейший вариант использования задачи для выполнения асинхронной работы по инициализации компонента.

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const fibonacci = useSignal<number[]>();
 
  useTask$(async () => {
    const size = 40;
    const array = [];
    array.push(0, 1);
    for (let i = array.length; i < size; i++) {
      array.push(array[i - 1] + array[i - 2]);
      await delay(100);
    }
    fibonacci.value = array;
  });
 
  return <p>{fibonacci.value?.join(', ')}</p>;
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

В данном примере

  • Функция useTask$() вычисляет число Фибоначчи по одной записи за 100 мс. Таким образом, 40 записей занимают 4 секунды.
  • useTask$() выполняется на сервере как часть SSR (результат может быть кэширован в CDN).
  • Поскольку useTask$() блокирует рендер, рендер HTML-страницы занимает 4 секунды.
  • Поскольку у этой задачи нет track(), она никогда не будет выполняться повторно, что делает её фактически кодом инициализации.
  • Поскольку этот компонент отображается только на сервере, useTask$() никогда не будет выполняться в браузере.

Обратите внимание, что useTask$() выполняется ДО фактического рендера и на сервере. Поэтому, если вам необходимо выполнить манипуляции с DOM, используйте вместо него useVisibleTask$(), который запускается в браузере после рендера.

Используйте useTask$(), когда вам необходимо:

  • Выполнение асинхронных задач перед рендером;
  • Выполнять код только один раз перед первым рендером компонента;
  • Программно выполнить код с побочными эффектами при изменении состояния.

Обратите внимание, если вы думаете о загрузке данных (например, используя fetch()) внутри useTask$, то подумайте об использовании вместо этого useResource$(). Этот API более эффективен с точки зрения использования потоковой передачи SSR и параллельной выборки данных.

При монтировании

Qwik не имеет специального хука "on-mount", поскольку useTask$() без track() эффективно ведёт себя как хук mount.

Это связано с тем, что useTask$ всегда запускается хотя бы раз при монтировании компонента.

import { component$, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
 
  useTask$(async () => {
    // Задача без `track` состояния эффективно ведет себя как хук `on mount`.
    console.log('Выполняется один раз при монтировании компонента на сервере ИЛИ на клиенте.');
  });
 
  return <div>Привет</div>;
});

Уникальность Qwik заключается в том, что компоненты монтируются только ОДИН раз на сервере и клиенте. Это свойство возобновляемости. Это означает, что если useTask$ запущен во время SSR, он не будет запущен снова в браузере, потому что Qwik не занимается гидратацией.

track()

Бывают случаи, когда желательно повторно запустить задачу при изменении состояния компонента. Это делается с помощью функции track(). Функция track() позволяет вам установить зависимость от состояния компонента на сервере (если его первоначальный рендер был там) и затем повторно выполнить задачу при изменении состояния в браузере (одна и та же задача никогда не будет выполнена дважды на стороне сервера).

Примечание: Если всё, что вы хотите сделать, это синхронно вычислить новое состояние из существующего, то вместо этого вам следует использовать useComputed$().

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const text = useSignal('Initial text');
  const delayText = useSignal('');
 
  useTask$(({ track }) => {
    track(() => text.value);
    const value = text.value;
    const update = () => (delayText.value = value);
    isServer
      ? update() // Во время рендера на сервере задержку не делаем.
      : delay(500).then(update); // Задержка в браузере.
  });
 
  return (
    <section>
      <label>
        Введите текст: <input bind:value={text} />
      </label>
      <p>Текст с задержкой: {delayText}</p>
    </section>
  );
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

На сервере:

  • Функция useTask$() выполняется на сервере, а функция track() устанавливает подписку на сигнал text.
  • Страница отображается.

В браузере:

  • Задаче useTask$() не нужно выполняться (или загружаться) сразу, поскольку Qwik знает, что задача подписана на сигнал text от сервера.
  • Когда пользователь набирает текст в поле ввода, сигнал text изменяется. Qwik знает, что useTask$() подписана на сигнал text, и именно в это время замыкание useTask$() попадает в JavaScript VM для выполнения.

Функция useTask$()

  • Функция useTask$() блокирует рендер до его завершения. Если вы не хотите блокировать рендер (как в данном случае), убедитесь, что задача решена, и запустите отложенную работу в отдельном несвязанном промисе (в нашем случае мы не ожидаем delay(), это приведёт к блокировке рендера).

Иногда требуется запускать код только на сервере или только в браузере. Этого можно достичь, используя флаги isServer и isBrowser, экспортируемые из @builder.io/qwik/build, как показано выше.

track() как функция

В приведённом выше примере track() использовалась для отслеживания определённого сигнала. Однако track() можно также использовать как функцию для отслеживания нескольких сигналов одновременно.

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const isUppercase = useSignal(false);
  const text = useSignal('');
  const delayText = useSignal('');
 
  useTask$(({ track }) => {
    const value = track(() =>
      isUppercase.value ? text.value.toUpperCase() : text.value.toLowerCase()
    );
    const update = () => (delayText.value = value);
    isServer
      ? update() // Во время рендера на сервере задержку не делаем.
      : delay(500).then(update); // Задержка в браузере.
  });
 
  return (
    <section>
      <label>
        Введите текст: <input bind:value={text} />
      </label>
      <label>
        Прописной <input type="checkbox" bind:checked={isUppercase} />
      </label>
      <p>Текст с задержкой: {delayText}</p>
    </section>
  );
});
 
function delay(time: number) {
  return new Promise((resolve) => setTimeout(resolve, time));
}

В этом примере track() принимает функцию, которая не только считывает сигнал, но и преобразует его значение в верхний/нижний регистр. Функция track() выполняет подписку на несколько сигналов и вычисляет их значение.

cleanup()

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

  • Функция cleanup() не вызывается при завершении задачи. Она вызывается только при запуске новой задачи или при удалении компонента.
  • Функция cleanup() вызывается на сервере после сериализации приложений в HTML.
  • Функция cleanup() не переносится с сервера в браузер (очистка предназначена для освобождения ресурсов на VM, где она запущена. Она не предназначена для передачи клиенту).

В этом примере показано, как реализовать функцию демпфирования с помощью функции cleanup().

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const text = useSignal('');
  const debounceText = useSignal('');
 
  useTask$(({ track, cleanup }) => {
    const value = track(() => text.value);
    const id = setTimeout(() => (debounceText.value = value), 500);
    cleanup(() => clearTimeout(id));
  });
 
  return (
    <section>
      <label>
        Введите текст: <input bind:value={text} />
      </label>
      <p>Текст с задержкой: {debounceText}</p>
    </section>
  );
});

useVisibleTask$()

Иногда задача должна выполняться только на клиенте и после рендера компонента. В этом случае следует использовать useVisibleTask$(). Функция useVisibleTask$() похожа на useTask$(), но она выполняется только в браузере и после первоначального рендера. useVisibleTask$() регистрирует хук, который будет выполняться, когда компонент становится видимым в области просмотра, он будет запущен хотя бы один раз в браузере, и он может быть реактивным и повторно выполняться при изменении некоторого отслеживаемого состояния.

useVisibleTask$() имеет следующие свойства:

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

Осторожно: Использовать useVisibleTask$() следует в крайнем случае, поскольку он сразу выполняет код в браузере. Qwik через возобновляемость делает всё возможное, чтобы задержать выполнение кода в браузере, а useVisibleTask$() - это аварийный люк, который следует использовать с осторожностью. Подробнее см. в раздел Лучшие практики. Если вам нужно запустить задачу в браузере, рассмотрите useTask$() с защитой от выполнения на сервере.

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const text = useSignal('Initial text');
  const isBold = useSignal(false);
 
  useTask$(({ track }) => {
    track(() => text.value);
    if (isServer) {
      return; // Защита от выполнения на сервере
    }
    isBold.value = true;
    delay(1000).then(() => (isBold.value = false));
  });
 
  return (
    <section>
      <label>
        Введите текст: <input bind:value={text} />
      </label>
      <p style={{ fontWeight: isBold.value ? 'bold' : 'normal' }}>
        Текст: {text}
      </p>
    </section>
  );
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

В приведённом выше примере useTask$() защищена флагом isServer. track() находится перед защитой, которая позволяет серверу установить подписку, но не выполняет никакого кода на сервере. Затем браузер выполняет useTask$(), как только сигнал text изменится.

В этом примере показано, как использовать useVisibleTask$() для инициализации часов в браузере только тогда, когда компонент часов становится видимым.

import {
  component$,
  useSignal,
  useVisibleTask$,
  type Signal,
} from '@builder.io/qwik';
 
export default component$(() => {
  const isClockRunning = useSignal(false);
 
  return (
    <>
      <div style="position: sticky; top:0">
        Прокрутите страницу, пока не увидите часы (текущий статус часов:
        {isClockRunning.value ? 'запущены' : 'не запущены'}.)
      </div>
      <div style="height: 200vh" />
      <Clock isRunning={isClockRunning} />
    </>
  );
});
 
const Clock = component$<{ isRunning: Signal<boolean> }>(({ isRunning }) => {
  const time = useSignal('paused');
  useVisibleTask$(({ cleanup }) => {
    isRunning.value = true;
    const update = () => (time.value = new Date().toLocaleTimeString());
    const id = setInterval(update, 1000);
    cleanup(() => clearInterval(id));
  });
  return <div>{time}</div>;
});

Обратите внимание, что функция часов useVisibleTask$() не выполняется, пока компонент <Clock> не становится видимым. Поведение по умолчанию useVisibleTask$() заключается в запуске задачи, когда компонент становится видимым. Это поведение реализуется через intersection observers.

Опция eagerness

Иногда желательно запускать useVisibleTask$() сразу, как только приложение загружается в браузер. В этом случае useVisibleTask$() должна выполняться в режиме ожидания. Это делается с помощью { strategy: 'document-ready' }.

import {
  component$,
  useSignal,
  useVisibleTask$,
  type Signal,
} from '@builder.io/qwik';
 
export default component$(() => {
  const isClockRunning = useSignal(false);
 
  return (
    <>
      <div style="position: sticky; top:0">
        Прокрутите страницу, пока не увидите часы (текущий статус часов:
        {isClockRunning.value ? 'запущены' : 'не запущены'}.)
      </div>
      <div style="height: 200vh" />
      <Clock isRunning={isClockRunning} />
    </>
  );
});
 
const Clock = component$<{ isRunning: Signal<boolean> }>(({ isRunning }) => {
  const time = useSignal('paused');
  useVisibleTask$(
    ({ cleanup }) => {
      isRunning.value = true;
      const update = () => (time.value = new Date().toLocaleTimeString());
      const id = setInterval(update, 1000);
      cleanup(() => clearInterval(id));
    },
    { strategy: 'document-ready' }
  );
  return <div>{time}</div>;
});

В этом примере часы в браузере сразу начинают идти, независимо от того, видны они или нет.

Дополнительно: Время выполнения и управление видимостью с помощью CSS

Внутренне useVisibleTask$ реализуется путём добавления атрибута на первый отрисованный компонент (либо возвращаемый компонент, либо, в случае фрагмента, его первый дочерний компонент). При стандартном eagerness это означает, что если первый отрисованный компонент будет скрыт, то задача не будет запущена.

Это означает, что с помощью CSS можно влиять на время выполнения задачи. Например, если задача должна выполняться только на мобильном устройстве, можно вернуть <div class="md:invisible" /> (если используется Tailwind CSS).

Это также означает, что нельзя снять с компонента видимую задачу - для этого можно вернуть фрагмент:

return (<>
  <div />
  <MyHiddenComponent hidden={!showSignal.value} />
</>)

Правила Use-хуков

При использовании хуков жизненного цикла необходимо придерживаться следующих правил:

  • Они могут быть вызваны только на корневом уровне component$ (не внутри блоков условий);
  • Они могут быть вызваны только в корне другого метода use*, допускающего композицию.
useHook(); // <-- ❌ не работает
 
export default component$(() => {
  useCustomHook(); // <-- ✅ работает
  if (condition) {
    useHook(); // <-- ❌ не работает
  }
  useTask$(() => {
    useNavigate(); // <-- ❌ не работает
  });
  const myQrl = $(() => useHook()); // <-- ❌ не работает
  return <button onClick$={() => useHook()}></button>; // <-- ❌ не работает
});
 
function useCustomHook() {
  useHook(); // <-- ✅ работает
  if (condition) {
    useHook(); // <-- ❌ не работает
  }
}

Участники

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

  • mhevery
  • manucorporat
  • wtlin1228
  • AnthonyPAlicea
  • the-r3aper7
  • sreeisalso
  • brunocrosier
  • harishkrishnan24
  • gioboa
  • bodhicodes
  • zanettin
  • blackpr
  • mrhoodz
  • ehrencrona
  • julianobrasil
  • adamdbradley
  • aendel