タスク

タスクは、コンポーネントの初期化またはコンポーネントの状態変更の一部として非同期操作を実行するためのものです。

注意: タスクは React の useEffect() に似ていますが、動作に関する既存の期待を抱かせないように、同じ名前を付けたくないほど違いがあります。主な違いは次のとおりです。

  • タスクは非同期です。
  • タスクはサーバーとブラウザーで実行されます。
  • タスクはレンダリング前に実行され、レンダリングをブロックできます。

useTask$() は、コンポーネントの初期化または状態変更の一部として、同期または非同期の作業を実行するためのデフォルトの API である必要があります。useTask$() で必要なことを達成できない場合にのみ、useVisibleTask$() または useResource$() の使用を検討する必要があります。

useTask$() の基本的なユースケースは、コンポーネントの初期化時に作業を実行することです。useTask$() には次のプロパティがあります。

  • サーバーまたはブラウザーのいずれかで実行できます。
  • レンダリング前に実行され、レンダリングをブロックします。
  • 複数のタスクが実行されている場合、登録された順序で順次実行されます。非同期タスクは、完了するまで次のタスクの実行をブロックします。

タスクは、コンポーネントの状態が変更されたときに作業を実行するためにも使用できます。この場合、追跡された状態が変更されるたびにタスクが再実行されます。以下を参照してください: track()

場合によっては、タスクをブラウザーでのみ、レンダリング後に実行する必要がある場合があります。その場合は、useVisibleTask$() を使用する必要があります。

注意: 非同期でデータをフェッチし、レンダリングをブロックしない必要がある場合は、useResource$() を使用する必要があります。useResource$() は、リソースの解決中にレンダリングをブロックしません。

ライフサイクル

再開可能性は「遅延実行」であり、サーバー上で「フレームワークの状態」(コンポーネントの境界など)を構築し、フレームワークを再度実行せずにクライアントに存在させる機能です。

クライアント側かサーバー側かというアプリケーション環境は、ユーザーの操作によって決まります。サーバーサイドレンダリングでは、アプリケーションは最初にサーバーでレンダリングされます。ユーザーがアプリケーションを操作すると、クライアント側で再開され、サーバーによって残された状態から継続します。このアプローチにより、対話に基づいて両方の環境を活用することで、効率的で応答性の高いユーザーエクスペリエンスが保証されます。

注意: ハイドレーションを使用するシステムでは、アプリケーションの実行は 2 回発生します。1 回はサーバー (SSR/SSG) で、もう 1 回はブラウザー (ハイドレーション) で発生します。これが、多くのフレームワークにブラウザーでのみ実行される「エフェクト」がある理由です。つまり、サーバーで実行されるコードは、ブラウザーで実行されるコードとは異なります。Qwik の実行は統合されており、コードがすでにサーバーで実行されている場合は、ブラウザーで再実行されません。

Qwik では、ライフサイクルステージは 3 つしかありません。

  • Task - レンダリング前および追跡された状態が変更されたときに実行されます。Tasks は順次実行され、レンダリングをブロックします。
  • 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$()

  • タイミング: コンポーネントの最初のレンダリング前、および追跡された状態が変更されたとき
  • 回数: 少なくとも 1 回
  • プラットフォーム: サーバーおよびブラウザー

useTask$() は、コンポーネントの作成時に実行されるフックを登録します。コンポーネントが最初にレンダリングされる場所に応じて、サーバーまたはブラウザのいずれかで少なくとも一度実行されます。

さらに、このタスクはリアクティブであり、追跡された 状態 が変更されると再実行されます。

タスクの後続の再実行は常にブラウザで発生することに注意してください。リアクティビティはブラウザのみの機能であるためです。

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

useTask$() が状態を追跡しない場合、コンポーネントが最初にレンダリングされる場所に応じて、サーバーまたはブラウザのいずれかで正確に一度実行されます(両方ではありません)。事実上、「マウント時」フックのように動作します。

useTask$() は、非同期コールバックが解決するまでコンポーネントのレンダリングをブロックします。言い換えれば、タスクは非同期であっても順番に実行されます。(一度に実行されるタスクは1つだけです)。

コンポーネントの初期化時に非同期処理を実行するための、最もシンプルなタスクの使用例を見てみましょう。

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ミリ秒ごとに1エントリのフィボナッチ数を計算します。したがって、40エントリのレンダリングには4秒かかります。
    • useTask$() は、SSRの一部としてサーバーで実行されます(結果はCDNにキャッシュされる場合があります)。
    • useTask$() はレンダリングをブロックするため、レンダリングされたHTMLページのレンダリングに4秒かかります。
  • このタスクには track() がないため、再実行されることはなく、事実上初期化コードになります。
    • このコンポーネントはサーバーでのみレンダリングされるため、useTask$() はブラウザでダウンロードまたは実行されることはありません。

useTask$() は、実際のレンダリングの前にサーバーで実行されることに注意してください。したがって、DOM操作を行う必要がある場合は、レンダリング後にブラウザで実行されるuseVisibleTask$() を代わりに使用してください。

以下が必要な場合は、useTask$() を使用します。

  • レンダリング前に非同期タスクを実行する
  • コンポーネントが最初にレンダリングされる前に一度だけコードを実行する
  • 状態が変更されたときに、プログラムで副作用コードを実行する

注:useTask$ 内で fetch() を使用してデータをロードすることを検討している場合は、代わりに useResource$() を使用することを検討してください。このAPIは、SSRストリーミングと並列データフェッチの活用という点でより効率的です。

マウント時

Qwikでは、他のいくつかのフレームワークのような特定の「マウント」ステップはありません。代わりに、コンポーネントは、Webサーバーまたはブラウザのいずれかで、必要な場所で直接起動します。これは、特定のデータを監視するために使用される内部の track 関数を使用しません。

useTask$ は、コンポーネントが最初にマウントされるときに、少なくとも一度は常に実行されます。

import { component$, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
 
  useTask$(async () => {
    // A task without `track` any state effectively behaves like a `on mount` hook.
    console.log('Runs once when the component mounts in the server OR client.');
  });
 
  return <div>Hello</div>;
});

Qwikのユニークな側面の1つは、コンポーネントがサーバーとクライアント全体で一度だけマウントされることです。これは、再開可能性の特性です。これは、サーバーサイドレンダリング(SSR)中に useTask$ が実行された場合、Qwikはハイドレーションを実行しないため、ブラウザで再度実行されないことを意味します。

track()

コンポーネントの状態が変更されたときにタスクを再実行することが望ましい場合があります。これは、track() 関数を使用することで実現します。track() 関数を使用すると、サーバーで最初にレンダリングされたときにコンポーネントの状態への依存関係を設定し、ブラウザで状態が変更されたときにタスクを再実行できます。同じタスクがサーバー上で2回実行されることはありません。

:既存の状態から新しい状態を同期的に計算したいだけの場合は、代わりに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() // don't delay on server render value as part of SSR
      : delay(500).then(update); // Delay in browser
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p>Delayed text: {delayText}</p>
    </section>
  );
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

サーバー上

  • useTask$() はサーバー上で実行され、track() 関数は text シグナルにサブスクリプションを設定します。
  • ページがレンダリングされます。

ブラウザ上

  • Qwikは、タスクがサーバー実行からの text シグナルにサブスクライブされていることを認識しているため、useTask$() を実行したり、積極的にダウンロードしたりする必要はありません。
  • ユーザーが入力ボックスに入力すると、text シグナルが変更されます。Qwikは、useTask$()text シグナルにサブスクライブされていることを認識しており、この時点で useTask$() クロージャが実行されるJavaScript VMに取り込まれます。

useTask$()

  • useTask$() は、完了するまでレンダリングをブロックします。レンダリングをブロックしたくない場合は、タスクが解決され、分離された非接続のPromiseで遅延処理を実行していることを確認してください。この例では、delay() がレンダリングをブロックするため、await しません。

サーバーまたはクライアントのいずれかでコードを実行する必要がある場合があります。これは、上記のように @builder.io/qwik/build からエクスポートされた isServer および isBrowser ブール値を使用することで実現できます。

関数としての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() // don't delay on server render value as part of SSR
      : delay(500).then(update); // Delay in browser
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <label>
        Is uppercase? <input type="checkbox" bind:checked={isUppercase} />
      </label>
      <p>Delay text: {delayText}</p>
    </section>
  );
});
 
function delay(time: number) {
  return new Promise((resolve) => setTimeout(resolve, time));
}

この例では、track() は、シグナルを読み取るだけでなく、その値を大文字/小文字に変換する関数を取ります。track() は複数のシグナルをサブスクライブし、それらの値を計算します。

cleanup()

タスクを実行するときに、クリーンアップ作業を実行する必要がある場合があります。新しいタスクがトリガーされると、前のタスクの cleanup() コールバックが呼び出されます。このコールバックは、コンポーネントがDOMから削除されたときにも呼び出されます。

  • 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>
        Enter text: <input bind:value={text} />
      </label>
      <p>Debounced text: {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; // Server guard
    }
    isBold.value = true;
    delay(1000).then(() => (isBold.value = false));
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p style={{ fontWeight: isBold.value ? 'bold' : 'normal' }}>
        Text: {text}
      </p>
    </section>
  );
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

上記の例では、useTask$()isServer によって保護されています。track() 関数はガードの前に配置されており、サーバーはサーバーでコードを実行せずにサブスクリプションを設定できます。クライアントは、text シグナルが変更されると、useTask$() を一度実行します。

この例は、時計コンポーネントが表示された場合にのみ、ブラウザで時計を初期化するために 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">
        Scroll to see clock. (Currently clock is
        {isClockRunning.value ? ' running' : ' not running'}.)
      </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 observer を通じて実装されます。

intersection observer は、<audio /> のように表示されていると見なされないコンポーネントでは実行されません。

オプション 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">
        Scroll to see clock. (Currently clock is
        {isClockRunning.value ? ' running' : ' not running'}.)
      </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$ は、最初にレンダリングされたコンポーネント(返されたコンポーネント、またはFragmentの場合はその最初の子)に属性を追加することによって実装されます。標準の eagerness では、これは、最初にレンダリングされたコンポーネントが非表示の場合、タスクは実行されないことを意味します。

これは、CSSを使用してタスクの実行時期に影響を与えることができることを意味します。たとえば、タスクをモバイルデバイスでのみ実行する必要がある場合は、<div class="md:invisible" /> を返すことができます(Tailwind CSSの場合)。

これは、可視タスクを使用してコンポーネントの非表示を解除できないことも意味します。そのためには、Fragmentを返すことができます。

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

フックの使用規則

ライフサイクルフックを使用する場合は、次の規則に従う必要があります。

  • component$ のルートレベルでのみ呼び出すことができます(条件付きブロック内ではありません)。
  • これらは、別のuse*メソッドのルートでのみ呼び出すことができ、構成を可能にします。
useHook(); // <-- ❌ does not work
 
export default component$(() => {
  useCustomHook(); // <-- ✅ does work
  if (condition) {
    useHook(); // <-- ❌ does not work
  }
  useTask$(() => {
    useNavigate(); // <-- ❌ does not work
  });
  const myQrl = $(() => useHook()); // <-- ❌ does not work
  return <button onClick$={() => useHook()}></button>; // <-- ❌ does not work
});
 
function useCustomHook() {
  useHook(); // <-- ✅ does work
  if (condition) {
    useHook(); // <-- ❌ does not work
  }
}

貢献者

このドキュメントの改善にご協力いただいたすべての貢献者に感謝します!

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