状態
状態管理は、あらゆるアプリにおいて重要な部分です。Qwik には、リアクティブと静的の 2 種類の状態があります。
- 静的な状態とは、シリアライズ可能なものです。文字列、数値、オブジェクト、配列など、あらゆるものです。
- 一方、リアクティブな状態は、
useSignal()
またはuseStore()
で作成されます。
Qwik の状態は必ずしもローカルなコンポーネントの状態ではなく、あらゆるコンポーネントでインスタンス化できるアプリの状態であることに注意することが重要です。
useSignal()
useSignal()
を使用して、リアクティブシグナル (状態の一種) を作成します。useSignal()
は初期値を受け取り、リアクティブシグナルを返します。
useSignal()
によって返されるリアクティブシグナルは、単一のプロパティ .value
を持つオブジェクトで構成されます。シグナルの value
プロパティを変更すると、それに依存するすべてのコンポーネントが自動的に更新されます。
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<button onClick$={() => count.value++}>
Increment {count.value}
</button>
);
});
上記の例は、useSignal()
をカウンターコンポーネントで使用して、カウントを追跡する方法を示しています。count.value
プロパティを変更すると、コンポーネントが自動的に更新されます。たとえば、上記の例のように、ボタンクリックハンドラーでプロパティが変更された場合などです。
注 シグナルの値を読み取るだけでよい場合は、シグナル全体を props として渡すのではなく、その値のみを渡してください。
⛔ 避けるべきこと:
const isClosedSig = useSignal(false);
return <Child isClosed={isClosedSig} />;
✅ 代わりにすべきこと:
const isClosedSig = useSignal(false);
return <Child isClosed={isClosedSig.value} />;
useStore()
useSignal()
と非常によく似ていますが、初期値としてオブジェクトを受け取り、リアクティビティはデフォルトでネストされたオブジェクトと配列にまで拡張されます。ストアは、複数の値を持つシグナル、または複数のシグナルで構成されたオブジェクトと考えることができます。
useStore(initialStateObject)
フックを使用して、リアクティブオブジェクトを作成します。初期オブジェクト (またはファクトリー関数) を受け取り、リアクティブオブジェクトを返します。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const state = useStore({ count: 0, name: 'Qwik' });
return (
<>
<button onClick$={() => state.count++}>Increment</button>
<p>Count: {state.count}</p>
<input
value={state.name}
onInput$={(_, el) => (state.name = el.value)}
/>
</>
);
});
注 リアクティビティが期待どおりに機能するためには、リアクティブオブジェクトへの参照を保持し、そのプロパティのみへの参照ではないことを確認してください。たとえば、
let { count } = useStore({ count: 0 })
を実行し、その後count
を変更しても、そのプロパティに依存するコンポーネントの更新はトリガーされません。
useStore()
は深いリアクティビティを追跡するため、ストア内の配列とオブジェクトもリアクティブになることを意味します。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const store = useStore({
nested: {
fields: { are: 'also tracked' },
},
list: ['Item 1'],
});
return (
<>
<p>{store.nested.fields.are}</p>
<button
onClick$={() => {
// Even though we are mutating a nested object, this will trigger a re-render
store.nested.fields.are = 'tracked';
}}
>
Clicking me works because store is deep watched
</button>
<br />
<button
onClick$={() => {
// Because store is deep watched, this will trigger a re-render
store.list.push(`Item ${store.list.length}`);
}}
>
Add to list
</button>
<ul>
{store.list.map((item, key) => (
<li key={key}>{item}</li>
))}
</ul>
</>
);
});
useStore()
がすべてのネストされたプロパティを追跡するためには、多くの Proxy オブジェクトを割り当てる必要があることに注意してください。ネストされたプロパティが多い場合は、パフォーマンス上の問題になる可能性があります。その場合は、deep: false
オプションを使用して、トップレベルのプロパティのみを追跡できます。
const shallowStore = useStore(
{
nested: {
fields: { are: 'also tracked' }
},
list: ['Item 1'],
},
{ deep: false }
);
動的なオブジェクトの変更の処理
アプリ内のどこかでレンダリング中に、オブジェクトのプロパティを動的に操作 (削除など) すると、問題が発生する可能性があります。これは、現在削除中のオブジェクトのプロパティに依存する値をコンポーネントがレンダリングする場合に発生する可能性があります。この状況を防ぐには、プロパティにアクセスするときにオプションのチェーンを使用します。たとえば、プロパティを削除しようとする場合は
delete store.propertyName;
オプションのチェーン ( ?. ) を使用して、コンポーネント内でこのプロパティに慎重にアクセスしてください。
const propertyValue = store.propertyName?.value;
メソッド
ストアにメソッドを提供するには、それらを QRL にし、this
を使用してストアを参照する必要があります。
import { component$, useStore, $, type QRL } from "@builder.io/qwik";
type CountStore = { count: number; increment: QRL<(this: CountStore) => void> };
export default component$(() => {
const state = useStore<CountStore>({
count: 0,
increment: $(function (this: CountStore) {
this.count++;
}),
});
return (
<>
<button onClick$={() => state.increment()}>Increment</button>
<p>Count: {state.count}</p>
</>
);
});
useStore()
でアロー関数ではなく通常のfunction(){}
を使用する必要がある理由をご存知ですか?これは、アロー関数は javascript でthis
への独自のバインディングを持っていないためです。つまり、アロー関数を使用してthis
にアクセスしようとすると、this.count
が別のオブジェクトのcount
を指す可能性があります 😱。
計算された状態
Qwik には、計算された値を作成する 2 つの方法があり、それぞれ異なるユースケースがあります (推奨順)。
-
useComputed$()
:useComputed$()
は、計算された値を作成するための推奨される方法です。計算された値がソース状態 (現在のアプリケーション状態) から同期的に純粋に派生できる場合に使用します。たとえば、文字列の小文字バージョンを作成したり、姓と名を組み合わせてフルネームを作成したりする場合などです。 -
useResource$()
:useResource$()
は、計算された値が非同期の場合、または状態がアプリケーションの外部から取得される場合に使用されます。たとえば、現在の場所 (アプリケーションの内部状態) に基づいて、現在の天気 (外部状態) をフェッチする場合などです。
上記で説明した計算値を作成する2つの方法に加えて、より低レベルな方法(useTask$()
)もあります。この方法は、新しいシグナルを生成するのではなく、既存の状態を変更したり、副作用を生み出したりします。
useComputed$()
他の状態から同期的に導出された値をメモ化するには、useComputed$
を使用します。
入力シグナルのいずれかが変更された場合にのみ値を再計算するため、他のフレームワークのmemo
に似ています。
import { component$, useComputed$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const name = useSignal('Qwik');
const capitalizedName = useComputed$(() => {
// it will automatically reexecute when name.value changes
return name.value.toUpperCase();
});
return (
<>
<input type="text" bind:value={name} />
<p>Name: {name.value}</p>
<p>Capitalized name: {capitalizedName.value}</p>
</>
);
});
注意
useComputed$()
は同期的なため、入力シグナルを明示的に追跡する必要はありません。
useResource$()
非同期的に導出される計算値を作成するには、useResource$()
を使用します。これはuseComputed$()
の非同期バージョンであり、値に加えてリソースの状態(ロード中、解決済み、拒否済み)が含まれます。
useResource$()
の一般的な使用例は、コンポーネント内の外部APIからデータをフェッチすることです。これは、サーバーまたはクライアントのいずれかで発生する可能性があります。
useResource$
フックは、<Resource />
とともに使用することを目的としています。<Resource />
コンポーネントは、リソースの状態に基づいて異なるUIをレンダリングする便利な方法です。
import {
component$,
Resource,
useResource$,
useSignal,
} from '@builder.io/qwik';
export default component$(() => {
const prNumber = useSignal('3576');
const prTitle = useResource$<string>(async ({ track }) => {
// it will run first on mount (server), then re-run whenever prNumber changes (client)
// this means this code will run on the server and the browser
track(() => prNumber.value);
const response = await fetch(
`https://api.github.com/repos/QwikDev/qwik/pulls/${prNumber.value}`
);
const data = await response.json();
return data.title as string;
});
return (
<>
<input type="number" bind:value={prNumber} />
<h1>PR#{prNumber}:</h1>
<Resource
value={prTitle}
onPending={() => <p>Loading...</p>}
onResolved={(title) => <h2>{title}</h2>}
/>
</>
);
});
注:
useResource$
について理解しておくべき重要な点は、最初のコンポーネントのレンダリング時に実行されるということです(useTask$
と同様)。多くの場合、コンポーネントがレンダリングされる前に、初期HTTPリクエストの一部としてサーバーでデータのフェッチを開始することが望ましいです。サーバーサイドレンダリング(SSR)の一部としてデータをフェッチすることは、一般的な推奨されるデータ読み込み方法であり、通常はrouteLoader$
APIによって処理されます。useResource$
は、ブラウザでデータをフェッチしたい場合に役立つ、より低レベルのAPIです。多くの点で、
useResource$
はuseTask$
に似ています。大きな違いは次のとおりです。
useResource$
は「値」を返すことができます。useResource$
は、リソースが解決される間、レンダリングをブロックしません。初期HTTPリクエストの一部としてデータを早期にフェッチする方法については、
routeLoader$
を参照してください。
注意: SSR中、
<Resource>
コンポーネントは、リソースが解決されるまでレンダリングを一時停止します。これにより、SSRはローディングインジケーターを表示せずにレンダリングされます。
高度な例
AbortController
、track
、cleanup
を使用してデータをフェッチするより完全な例。この例では、ユーザーが入力したクエリに基づいてジョークのリストをフェッチし、保留中のリクエストを中止することを含め、クエリの変更に自動的に反応します。
import {
component$,
useResource$,
Resource,
useSignal,
} from '@builder.io/qwik';
export default component$(() => {
const query = useSignal('busy');
const jokes = useResource$<{ value: string }[]>(
async ({ track, cleanup }) => {
track(() => query.value);
// A good practice is to use `AbortController` to abort the fetching of data if
// new request comes in. We create a new `AbortController` and register a `cleanup`
// function which is called when this function re-runs.
const controller = new AbortController();
cleanup(() => controller.abort());
if (query.value.length < 3) {
return [];
}
const url = new URL('https://api.chucknorris.io/jokes/search');
url.searchParams.set('query', query.value);
const resp = await fetch(url, { signal: controller.signal });
const json = (await resp.json()) as { result: { value: string }[] };
return json.result;
}
);
return (
<>
<label>
Query: <input bind:value={query} />
</label>
<button>search</button>
<Resource
value={jokes}
onPending={() => <>loading...</>}
onResolved={(jokes) => (
<ul>
{jokes.map((joke, i) => (
<li key={i}>{joke.value}</li>
))}
</ul>
)}
/>
</>
);
});
上記の例でわかるように、useResource$()
は、データとリソースの状態を含む、リアクティブなPromiseのように機能するResourceReturn<T>
オブジェクトを返します。
状態resource.loading
は、次のいずれかになります。
false
- データはまだ利用できません。true
- データは利用可能です。(解決済みまたは拒否済みのいずれか。)
useResource$()
に渡されるコールバックは、useTask$()
コールバックが完了した直後に実行されます。詳細については、ライフサイクルセクションを参照してください。
<Resource />
<Resource />
は、useResource$()
とともに使用することを目的としたコンポーネントであり、リソースが保留中、解決済み、または拒否されたかに応じて異なるコンテンツをレンダリングします。
<Resource
value={weatherResource}
onPending={() => <div>Loading...</div>}
onRejected={() => <div>Failed to load weather</div>}
onResolved={(weather) => {
return <div>Temperature: {weather.temp}</div>;
}}
/>
useResource$()
を使用する場合、<Resource />
は必須ではないことに注意してください。これは、リソースの状態をレンダリングするための便利な方法にすぎません。
この例では、useResource$
を使用してagify.io APIへのフェッチ呼び出しを実行する方法を示します。これは、ユーザーが入力した名前に基づいてその人の年齢を推測し、ユーザーが名前入力を変更するたびに更新されます。
import {
component$,
useSignal,
useResource$,
Resource,
} from '@builder.io/qwik';
export default component$(() => {
const name = useSignal<string>();
const ageResource = useResource$<{
name: string;
age: number;
count: number;
}>(async ({ track, cleanup }) => {
track(() => name.value);
const abortController = new AbortController();
cleanup(() => abortController.abort('cleanup'));
const res = await fetch(`https://api.agify.io?name=${name.value}`, {
signal: abortController.signal,
});
return res.json();
});
return (
<section>
<div>
<label>
Enter your name, and I'll guess your age!
<input onInput$={(ev, el) => (name.value = el.value)} />
</label>
</div>
<Resource
value={ageResource}
onPending={() => <p>Loading...</p>}
onRejected={() => <p>Failed to person data</p>}
onResolved={(ageGuess) => {
return (
<p>
{name.value && (
<>
{ageGuess.name} {ageGuess.age} years
</>
)}
</p>
);
}}
/>
</section>
);
});
状態の受け渡し
Qwikの優れた機能の1つは、状態を他のコンポーネントに渡すことができることです。ストアへの書き込みは、ストアから読み取るコンポーネントのみを再レンダリングします。
状態を他のコンポーネントに渡すには、次の2つの方法があります。
- propsを使用して子コンポーネントに明示的に状態を渡すか、
- コンテキストを介して暗黙的に状態を渡します。
propsの使用
状態を他のコンポーネントに渡す最も簡単な方法は、propsを介して渡すことです。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const userData = useStore({ count: 0 });
return <Child userData={userData} />;
});
interface ChildProps {
userData: { count: number };
}
export const Child = component$<ChildProps>(({ userData }) => {
return (
<>
<button onClick$={() => userData.count++}>Increment</button>
<p>Count: {userData.count}</p>
</>
);
});
コンテキストの使用
コンテキストAPIは、propsを介して渡すことなく(つまり、プロップドリリングの問題を回避)、コンポーネントに状態を渡す方法です。ツリー内のすべての子孫コンポーネントは、その状態への参照に自動的にアクセスでき、読み取り/書き込みアクセスが可能です。
詳細については、コンテキストAPIを確認してください。
import {
component$,
createContextId,
useContext,
useContextProvider,
useStore,
} from '@builder.io/qwik';
// Declare a context ID
export const CTX = createContextId<{ count: number }>('stuff');
export default component$(() => {
const userData = useStore({ count: 0 });
// Provide the store to the context under the context ID
useContextProvider(CTX, userData);
return <Child />;
});
export const Child = component$(() => {
const userData = useContext(CTX);
return (
<>
<button onClick$={() => userData.count++}>Increment</button>
<p>Count: {userData.count}</p>
</>
);
});
noSerialize()
Qwikは、すべてのアプリケーションの状態が常にシリアライズ可能であることを保証します。これは、Qwikアプリケーションに再開可能性プロパティがあることを保証するために重要です。
シリアライズできないデータを保存する必要がある場合があります。noSerialize()
は、Qwikにマークされた値のシリアライズを試みないように指示します。たとえば、Monaco editorなどのサードパーティライブラリへの参照は、シリアライズできないため、常にnoSerialize()
が必要になります。
値が非シリアライズ可能としてマークされている場合、その値は、SSRからクライアントでアプリケーションを再開するなど、シリアライズイベントを生き残れません。この状況では、値はundefined
に設定され、クライアントで値を再初期化するのは開発者の責任です。
import {
component$,
useStore,
useSignal,
noSerialize,
useVisibleTask$,
type NoSerialize,
} from '@builder.io/qwik';
import type Monaco from './monaco';
import { monacoEditor } from './monaco';
export default component$(() => {
const editorRef = useSignal<HTMLElement>();
const store = useStore<{ monacoInstance: NoSerialize<Monaco> }>({
monacoInstance: undefined,
});
useVisibleTask$(() => {
const editor = monacoEditor.create(editorRef.value!, {
value: 'Hello, world!',
});
// Monaco is not serializable, so we can't serialize it as part of SSR
// We can however instantiate it on the client after the component is visible
store.monacoInstance = noSerialize(editor);
});
return <div ref={editorRef}>loading...</div>;
});