Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

Vueにおけるインジェクションの浅いダイブ

リフレッシュ#

ライブラリの著者は、コンポーネントやコンポジション関数に複雑なコンテキスト情報を伝達するために、頻繁に provide/inject API を使用します。依存関係を消費者によって明示的に渡す必要がある props とは異なり、依存性注入は消費者がそれを認識することなく、シームレスに依存関係を渡すことができます。これは、依存関係が消費者にとって見えなくなり、不要な複雑さを効果的に抽象化することを意味します。

import { provide, ref } from 'vue';
export function useAppUserContext() {
  const user = ref(null);
  onMounted(() => {
    // ログイン中のユーザーを取得
    user.value = await fetch('https://tes.t/me').json();
  });
  function logout() {
    user.value = null;
  }
  function update(value) {
    user.value = { ...user.value, ...value };
  }
  const ctx = {
    user,
    update,
    logout
  };
  provide('AUTH_USER', ctx);
  return ctx;
}

この孤立した関数は、おそらくあなたのルートアプリコンポーネントの setup 関数内で呼び出されるでしょう。これにより、すべての子コンポーネントはユーザーコンテキストを簡単に注入し、必要に応じて利用できます。これは多くの点でストアに似ていますが、ユーザーがそれらの強力な反復を作成できるようにするメカニズムとしても機能します。

インジェクションキーの使用#

アプリで使用するすべてのインジェクションキーを含むルート injectionKeys.js ファイルを宣言し、インラインキー値の使用を防ぎます。

export const AUTH_USER_KEY = '認証されたユーザー';
export const CART_KEY = 'ユーザーのカート';

シンボルは、説明を付けることができ、作成されると常にユニークな値です。これは、オブジェクトキーとしても使用できます。つまり、ここでインジェクションキーとして使用できます。

export const AUTH_USER_KEY = Symbol('ユーザー');
// 後でいくつかのインジェクション...
export const CURRENT_USER_KEY = Symbol('ユーザー');

TypeScript の使用#

Vue は、provide/inject が渡される値の型を推論し、チェックできるようにする InjectionKey<TValue> という型を公開しています。以下は簡単な例です。

// types.ts
interface UserCartContext {
  items: CartItem[];
  total: number;
}
// injectionKeys.ts
import { InjectionKey } from 'vue';
import { UserCartContext } from '@/types';
const CART_KEY: InjectionKey<UserCartContext> = Symbol(
  'ユーザーのカートコンテキスト'
);

これにより、provide と inject の両方のレベルで型チェックが行われます。これは、あきらめるべきではありません。

import { provide, inject, reactive } from 'vue';
import { CART_KEY } from '@/injectionKeys';
// ❌ 型エラー
provide(CART_KEY, {});
const cart = reactive({ items: [], total: 0 });
// ✅
provide(CART_KEY, cart);
const cart = inject(CART_KEY);
// ❌ 型エラー
cart?.map(...);
// ✅
cart?.items.map(...)

inject 関数は、undefined と共に解決された型を生成することに注意する価値があります。これは、インジェクションが解決されない可能性があるためです。この状況をどのように処理するかはユーザー次第です。undefined を排除するには、inject 関数にフォールバック値を渡す必要があります。ここで興味深いのは、フォールバック値も型チェックを通過する必要があることです。

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
// ⛔️ 型 'string' の引数は割り当て可能ではありません...
const product = inject(ProductKey, 'nope');
// ✅ 型チェックが通過
const product = inject(ProductKey, { name: '', price: 0 });

プレーンな値の型を提供することはできますが、通常、これらの値の変更に反応する必要があるため、それほど有用ではありません。一般的な型を使用して、リアクティブなインジェクションを作成することもできます。
ref で作成されたリアクティブな refs については、一般的な Ref 型を使用して InjectionKey の型を指定できます。これは、ネストされた一般的な型です。

// types.ts
interface Product {
  name: string;
  price: number;
}
// symbols.ts
import { InjectionKey, Ref } from 'vue';
import { Product } from '@/types';
const ProductKey: InjectionKey<Ref<Product>> = Symbol('製品');
import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey); // 型は Ref<Product> | undefined
product?.value; // 型は Product

インジェクションの要求#

デフォルトでは、Vue はインジェクションが解決されなかった場合に警告を表示します。Vue は、インジェクションが見つからない場合にエラーをスローすることを選択できましたが、Vue はインジェクションが required かどうかを仮定することはできないため、未解決のインジェクションと undefined 値を理解するのはあなた次第です。

function injectStrict<T>(key: InjectionKey<T>, fallback?: T) {
  const resolved = inject(key, fallback);
  if (!resolved) {
    throw new Error(`Could not resolve ${key.description}`);
  }
  return resolved;
}
import { injectStrict } from '@/utils';
import { USER_CART_KEY } from '@/injectionKeys';
const cart = injectStrict(USER_CART_KEY);
// ここで `cart.items.map` にアクセスするのは安全です
cart.items.map(...);

インジェクションスコープ#

import { getCurrentInstance, inject, InjectionKey } from 'vue';
export function injectWithSelf<T>(
  key: InjectionKey<T>
): T | undefined {
  const vm = getCurrentInstance() as any;
  return vm?.provides[key as any] || inject(key);
}

この関数が行うのは、提供された依存関係を inject で解決しようとする前に、同じコンポーネントの提供された依存関係内でそれを見つけようとすることです。そうでなければ、inject と同じ動作をします。

シングルトンインジェクション#

import { provide } from 'vue';
import { CURRENT_USER_CTX } from '@/injectionKeys';
import { injectWithSelf } from '@/utils';
export function useAppUserContext() {
  const existingContext = injectWithSelf(CURRENT_USER_CTX, null);
  if (existingContext) {
    return existingContext;
  }
  const user = ref(null);
  onMounted(() => {
    // ログイン中のユーザーを取得
    user.value = await fetch('https://tes.t/me').json();
  });
  function logout() {
    user.value = null;
  }
  function update(value) {
    user.value = { ...user.value, ...value };
  }
  const ctx = {
    user,
    update,
    logout
  };
  provide(CURRENT_USER_CTX, ctx);
  return ctx;
}

このアプローチを実装することで、useAppUserContext を何度呼び出しても、コードは一度だけ実行されます。その後、すべての他の呼び出しは、すでに注入されたインスタンスを使用します。その結果、コンポーネント内で inject を使用する必要はなくなります。このパターンに従うと、コンテキスト関数を自分自身に注入することができ、inject やカスタマイズされたバージョンを直接使用するよりも、はるかに直感的でユーザーフレンドリーです。この手順は、単一のコンポーネントツリー内でインジェクションが一度だけ作成されることを保証します。

参考文献:
https://logaretm.com/blog/making-the-most-out-of-vuejs-injections/
https://logaretm.com/blog/type-safe-provide-inject/

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。