ziflux-expert について
Angular の `resource()` API で SWR キャッシュを使う開発者向けの、ziflux 専門スキルです。キャッシュのライフサイクル、API、機能構成、よくある実装上の注意点を説明します。
主な機能
- 01`cachedResource` と `cachedMutation` の説明
- 02SWR キャッシュの既定値設定
- 03階層型キャッシュキーの設計
- 04楽観的更新とロールバック
- 05よくあるキャッシュの誤りの診断
活用シーン
- 01API、Store、コンポーネントで Angular 機能を構成
- 02Angular のビューにキャッシュ付きリソース読み込みを追加
- 03キャッシュ無効化とロールバックで変更を処理
- 04古いデータやインジェクションコンテキストのエラーを調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ neogenz/ ziflux --skill ziflux-expertインストール後、Claude Code で/ziflux-expertを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/api-reference.mdpatterns.mdreview-and-debug.md詳細
ziflux のエキスパート
これから ziflux のエキスパートとして対応します。ziflux は、Angular の resource() API に SWR(stale-while-revalidate)キャッシュを追加する Angular 22+ ライブラリです。依存関係はゼロです。シグナルネイティブです。状態管理ライブラリではありません。Angular のシグナルと resource() が状態レイヤーそのものです。ziflux が補うのは、データのライフサイクル(fresh → stale → expired)という、たった 1 つのギャップです。
API は、ドキュメントを読まなくても、あらゆる Angular 開発者が使い方を推測できるように設計されています。resource() を知っていれば、cachedResource() もわかります。
ドメインパターン
すべての機能は、厳格な 3 ファイル構成に従います。これは必須です。
feature.api.ts — providedIn: 'root'。DataCache を所有し、HTTP メソッドを公開します。
@Injectable({ providedIn: 'root' })
export class OrderApi {
readonly #http = inject(HttpClient);
readonly cache = new DataCache({ name: 'orders' });
getOrders(filters: OrderFilters) {
return this.#http.get<Order[]>('/api/orders', { params: filters });
}
getOrder(id: string) {
return this.#http.get<Order>(`/api/orders/${id}`);
}
createOrder(order: NewOrder) {
return this.#http.post<Order>('/api/orders', order);
}
}
feature.store.ts — ルートスコープの @Injectable()。cachedResource と cachedMutation を接続します。
@Injectable()
export class OrderListStore {
readonly #api = inject(OrderApi);
readonly orders = cachedResource({
cache: this.#api.cache,
cacheKey: ['orders', 'list'],
loader: () => this.#api.getOrders({}),
});
readonly createOrder = cachedMutation({
cache: this.#api.cache,
mutationFn: (order: NewOrder) => this.#api.createOrder(order),
invalidateKeys: () => [['orders']],
});
}
feature.component.ts — ストアを注入し、テンプレートでシグナルを読み取ります。
@Component({
providers: [OrderListStore],
template: `
@if (store.orders.isInitialLoading()) {
<spinner />
} @else {
@for (order of store.orders.value(); track order.id) {
<order-card [order]="order" />
}
}
`,
})
export class OrderListComponent {
readonly store = inject(OrderListStore);
}
厳守するルール:
- コンポーネントから API サービスを直接注入することは絶対にありません
- HTTP ロジックは API サービスのみに記述します
- ストアで
DataCacheをインスタンス化することは絶対にありません。this.#api.cacheを読み取ります DataCacheは必ずprovidedIn: 'root'のサービスに置きます(画面遷移後も維持されます)
API クイックリファレンス
provideZiflux(config?, ...features)
app.config.ts で一度だけ呼び出します。グローバルなデフォルト値を設定します。
provideZiflux({ staleTime: 60_000, expireTime: 300_000 }, withDevtools())
DataCache
メモリ内 SWR キャッシュです。インジェクションコンテキスト内で作成する必要があります。
readonly cache = new DataCache({ name: 'orders', staleTime: 30_000, expireTime: 300_000 });
cache.get<T>(key: string[], opts?): { data: T; fresh: boolean } | null
cache.set<T>(key: string[], data: T): void
cache.invalidate(prefix: string[]): void // marks stale, never deletes
cache.deduplicate<T>(key: string[], fn): Promise<T> // one in-flight per key
cache.prefetch<T>(key: string[], fn): Promise<void>
cache.wrap<T>(key: string[], obs$): Observable<T> // tap → set
cache.clear(): void
cache.cleanup(): number // evict expired entries
cache.inspect(): CacheInspection<unknown>
cache.version: Signal<number> // bumps on invalidate/clear
cachedResource<T, P>(options)
SWR 対応の Angular resource() です。インジェクションコンテキスト内で呼び出す必要があります。
cachedResource({
cache: this.#api.cache,
cacheKey: params => ['orders', 'details', params.id],
params: () => ({ id: this.orderId() }), // undefined suspends (status: 'idle')
loader: ({ params, abortSignal }) => this.#api.getOrder(params.id),
defaultValue: [], // optional; makes value() never undefined
staleTime: 10_000, // optional per-resource override
retry: { maxRetries: 3, baseDelay: 1000 }, // optional
refetchInterval: 30_000, // optional polling, ignores staleTime
refetchOnWindowFocus: true, // optional; respects staleTime, browser-only
refetchOnReconnect: true, // optional; respects staleTime, browser-only
id: 'orders', // optional; reuse the SSR value via TransferState
})
CachedResourceRef<T> を返します。
value: Signal<T | undefined>— SWR 対応です。再検証中も古いデータを表示しますstatus: Signal<ResourceStatus>—'idle'|'loading'|'reloading'|'resolved'|'error'|'local'isLoading: Signal<boolean>— フェッチ中はtrueですisStale: Signal<boolean>— バックグラウンドで再取得中に古いデータを表示している場合はtrueですisInitialLoading: Signal<boolean>— キャッシュが空の場合のみtrueです(スピナーにはこれを使用します)error: Signal<Error | undefined>— Angular のresource()が公開するものと同じ型ですhasValue(): this is Omit<CachedResourceRef<T>, 'value'> & { value: Signal<T> }— 型ガードです。value()の型を絞り込みますreload(): boolean—staleTimeの期間内でも、常にネットワークにアクセスしますset(value: T): void— 楽観的更新です。DataCache にも書き込みます(ステータスは'local'になります)update(updater: (T | undefined) => T): void— 楽観的更新です。DataCache にも書き込みますdestroy(): void
cachedMutation<A, R, C>(options)
ミューテーションのラッパーです。インジェクションコンテキストは必要ありません。
cachedMutation({
cache: this.#api.cache,
mutationFn: (order: NewOrder) => this.#api.createOrder(order),
invalidateKeys: (args, result) => [['orders']],
onMutate: (args) => { /* optimistic update; return context */ },
onSuccess: (result, args) => { /* after invalidation */ },
onError: (error, args, context) => { /* rollback with context */ },
})
CachedMutationRef<A, R> を返します。
mutate(...args): Promise<R | undefined>— 拒否されることはありません。エラーはerrorシグナルに渡されますstatus: Signal<CachedMutationStatus>—'idle'|'pending'|'success'|'error'isPending: Signal<boolean>error: Signal<unknown>data: Signal<R | undefined>reset(): void
引数が void の場合:A = void のときは、引数なしで mutation.mutate() を呼び出します。
anyLoading(...signals: Signal<boolean>[]): Signal<boolean>
ローディングシグナルをまとめます。computed(() => signals.some(s => s()))。インジェクションコンテキストは必要ありません。
withDevtools(config?)
provideZiflux() 用の機能関数です。CacheRegistry とコンソールログを有効にします。
provideZiflux(config, withDevtools({ logOperations: true }))
ZifluxDevtoolsComponent
スタンドアロンコンポーネントです。セレクター:<ziflux-devtools />。isDevMode() により、本番環境では自動的に非表示になります。
SWR のライフサイクル
1. Cold cache → loader fires → status: 'loading' → isInitialLoading: true
2. Data arrives → cache.set() → status: 'resolved' → value has data
3. Navigate away → DataCache persists (root-scoped)
4. Navigate back → cache.get() returns fresh → NO loader call → instant render
5. Time passes → entry becomes stale
6. Next read → stale data shown immediately → background refetch starts
→ status: 'reloading' → isStale: true → isInitialLoading: false
7. Fresh data arrives → value updates → isStale: false
重要な UX 上のポイントは、isInitialLoading がスピナーを制御すること(キャッシュが空の場合のみ)です。isStale は状態を示すだけです。古いデータも引き続き表示されるため、ユーザーはすぐにコンテンツを見ることができます。
キャッシュキーの設計
キーは JSON.stringify() でシリアル化される string[] です。invalidate() のプレフィックスマッチングでは JSON.stringify(prefix).slice(0, -1) を使用します。
// Good: hierarchical keys
['orders', 'list']
['orders', 'details', orderId]
['orders', 'list', JSON.stringify(filters)]
// invalidate(['orders']) → invalidates ALL order-related entries
// invalidate(['orders', 'list']) → invalidates only the list
注意点: invalidate(['order']) は ['orders'] に一致しません。JSON のプレフィックス ["order" は ["orders" に一致しないためです。誤ったキャッシュ無効化を防ぐため、これは仕様です。
空のプレフィックス invalidate([]) は何もしません。 キャッシュを完全に消去するには cache.clear() を使用してください。
主なアンチパターン
- インジェクションコンテキスト外での DataCache → NG0203。クラスフィールド、コンストラクター、または
runInInjectionContext内で使用する必要があります。 - ルートスコープのサービスでの DataCache → ナビゲーション時にキャッシュが破棄され、SWR が機能しなくなります。必ず
providedIn: 'root'を指定してください。 staleTime > expireTime→ コンストラクターで例外が発生します。invalidate()が削除すると期待する → stale としてマークされます。get()は引き続き{ data, fresh: false }を返します。- 全件削除に
invalidate([])を使う → 何も起こりません。clear()を使用してください。 ref.value.set()→valueは読み取り専用のSignalであり、WritableSignalではありません。ref.set()またはref.update()を使用してください。await mutate()の後に成功を確認する →mutate()は拒否されません。mutation.error()シグナルを確認するか、戻り値を取得してください(undefinedはエラーを示します)。- コンポーネントで API サービスを直接インジェクトする → ドメインパターンに反します。コンポーネント → ストア → API の順にしてください。
- 状態管理に Subjects/Observables を使う → 禁止されています。Signals のみを使用してください。
params: () => undefinedを忘れると一時停止する → リソースは'idle'のままとなり、ローダーは実行されません。
楽観的更新
コンテキストを使用した onMutate → onError パターン:
readonly deleteItem = cachedMutation<string, void, Item[]>({
mutationFn: (id) => this.#api.deleteItem(id),
cache: this.#api.cache,
invalidateKeys: () => [['items']],
onMutate: (id) => {
const previous = this.items.value()!;
this.items.update(items => items!.filter(i => i.id !== id));
return previous; // context for rollback
},
onError: (_err, _id, previous) => {
if (previous) this.items.set(previous); // rollback
},
});
同時ミューテーションのセマンティクス
完了順ではなく、呼び出し順で最新のものが優先されます。2 つの mutate() 呼び出しが重複した場合:
- どちらも
mutationFnを実行します - 成功時にはどちらもキャッシュを無効化します(サーバーの状態が変化したため)
- 最後に呼び出されたものだけがシグナルを更新し、
onSuccessを実行します - 先に呼び出されたものの結果は、シグナルから通知なく破棄されます
設定の優先順位
コンストラクター引数 > provideZiflux() のグローバル設定 > ハードコードされたデフォルト値(staleTime:30s、expireTime:5min)。
参考資料
詳しい情報については、以下の参考ファイルをお読みください。
references/api-reference.md— 型シグネチャ全体、すべての設定オプション、DataCacheOptions の検証ルール、RetryConfig の詳細、CacheInspection の構造、またはエッジケースの動作を確認する場合にお読みください。references/patterns.md— ziflux を使った新機能の実装、楽観的更新の設定、キャッシュキー階層の設計、ポーリング/リトライの設定、または provideZiflux のセットアップを行う場合にお読みください。references/review-and-debug.md— ziflux を使ったコードのレビュー、キャッシュの問題(古いデータ、無効化の欠落、NG0203 エラー)のデバッグ、ziflux コードのテスト作成、または ziflux の使用箇所の整理を行う場合にお読みください。