オプショナルリアクティビティ#
import { Ref, ref } from 'vue';
type MaybeRef<T> = Ref<T> | T;
// 使用例
// ✅ 有効
const raw: MaybeRef<number> = 1;
// ✅ 有効
const reffed: MaybeRef<number> = ref(1);
isRef(raw); // false
isRef(reffed); // true
unref(raw); // 1
unref(reffed); // 1
例:
import { unref, ref, isRef, onMounted, watch } from 'vue';
import { MaybeRef } from '@/types';
async function fetchProduct(id: number) {
// APIから製品を返す
return fetch(`/api/products/${id}`).then((res) => res.json());
}
export function useProduct(id: MaybeRef<number>) {
const product = ref(null);
onMounted(async () => {
product.value = await fetchProduct(unref(id));
});
if (isRef(id)) {
watch(id, async (newId) => {
product.value = await fetchProduct(newId);
});
}
return product;
}
import { useProduct } from '@/composables/products';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
// マウント時に一度だけ取得
const product = useProduct(route.params.productId);
// パラメータが変更されるたびに同期
const product = useProduct(
computed(() => route.params.productId)
);
リファレンスの再パッキングを避ける#
import { reactive, watch } from 'vue';
const obj = reactive({
id: 1,
});
// ✅ 動作します!
watch(obj, () => {
// ...
});
// ❌ 動作しません
watch(obj.id, () => {
// ...
});
最後の watch
はうまく機能しません。なぜなら props.id
にアクセスすると、非リアクティブなバージョンが得られるからです。これは生のプロップ値です。したがって、リアクティブに保つためには、いくつかのテクニックを使う必要があります。
import { reactive, watch, toRef, toRefs, computed } from 'vue';
const obj = reactive({
id: 1,
});
// すべてのエントリをリファレンスに変換
const objRef = toRefs(obj);
watch(objRef.id, () => {
//...
});
// それを分解することもできます
const { id: idRef } = toRefs(obj);
watch(idRef, () => {
//...
});
// 単一のエントリをそのリアクティブバージョンに変換
const idRef = toRef(obj, 'id');
watch(idRef, () => {
//...
});
// 計算プロパティで値を抽出するだけ
const idComp = computed(() => obj.id);
watch(idComp, () => {
//...
});
上記のすべてのステートメントは、リアクティブオブジェクト値または任意のオブジェクトリファレンスからリアクティブな id
値を生成します。しかし、これには問題があります。ユーザーは関数に渡す前にリアクティブデータを適切に構造化する必要があり、これにより利用可能なパッキングメカニズムのいずれかを使用することを強いられます。
根本的な問題は、このプロセスが消費者に値をアンパックし、リアクティビティを維持したい場合は再パックすることを強制することです。このような慣行は、冗長性の増加を引き起こすことが多く、私はこれを「リファレンスの再パッキング」と呼んでいます。
解決策#
import { Ref } from 'vue';
// 生の値またはリファレンス
export type MaybeRef<T> = Ref<T> | T;
// 生の値にはできません
export type LazyOrRef<T> = Ref<T> | (() => T);
// リファレンス、ゲッター、または生の値になれます
export type MaybeLazyRef<T> = MaybeRef<T> | (() => T);
import { unref } from 'vue';
export function unravel<T>(value: MaybeLazyRef<T>): T {
if (typeof value === 'function') {
return value();
}
return unref(value);
}
export function isWatchable<T>(
value: MaybeLazyRef<T>
): value is LazyOrRef<T> {
return isRef(value) || typeof value === 'function';
}
例:
import { ref, onMounted, watch } from 'vue';
import { unravel, isWatchable } from '@/utils';
import { MaybeLazyRef } from '@/types';
async function fetchProduct(id: number) {
// APIから製品を返す
return fetch(`/api/products/${id}`).then((res) => res.json());
}
export function useProduct(id: MaybeLazyRef<number>) {
const product = ref(null);
onMounted(async () => {
product.value = await fetchProduct(unravel(id));
});
if (isWatchable(id)) {
// ゲッタ関数またはリファレンスの両方が監視可能なので動作します
watch(id, async (newId) => {
product.value = await fetchProduct(newId);
});
}
return product;
}
import { useRoute } from 'vue-router';
import { useProduct } from '@/composables/products';
const route = useRoute();
// マウント時に一度だけ取得
const product = useProduct(route.params.productId);
// パラメータが変更されるたびに同期
const product = useProduct(() => route.params.productId);
リアクティビティを要求する#
以前の例から離れて、usePositionFollower
関数を構築しようとしているとしましょう。この関数は、x,y 座標で示されたリアクティブな位置引数のみを受け入れます。非リアクティブな位置を取得することは無意味です。なぜなら、追従するものがないからです。
そのため、ユーザーにリアクティブな値、より具体的にはリアクティブな式のみを提供するように通知することが有益です。これにより MaybeLazyRef
の使用が排除されますが、以前にこの理由のために作成した LazyOrRef
を思い出してください。これは isWatchable
関数と一緒に機能します。
import { h, defineComponent } from 'vue';
import { LazyOrRef } from '@/types';
import { unravel } from '@/utils';
export function usePositionFollower(
position: LazyOrRef<{ x: number; y: number }>
) {
const style = computed(() => {
const { x, y } = unravel(position);
return {
position: 'fixed',
top: 0,
left: 0,
transform: `translate3d(${x}px, ${y}px, 0)`,
};
});
const Follower = defineComponent(
(props, { slots }) =>
() =>
h('div', { ...props, style: style.value }, slots)
);
return Follower;
}
const { x, y } = useMouse();
const Follower = usePositionFollower(() => ({
x: x.value,
y: y.value,
}));