Introduction
Incorporating signals into Vue applications requires careful consideration of interop techniques, async patterns, and common pitfalls. This guide outlines how to effectively manage these aspects, ensuring a seamless integration process.
Observing with watch and watchEffect
When using Vue's watch and watchEffect, it's crucial to separate concerns by observing only values (refs). To bridge a signal into a Vue ref, use useSignalRef() before employing watch or watchEffect. For data-layer side effects such as requests or logging, rely on createEffect without mixing in Vue's tracking system.
Correct Usage Example
// Convert to Vue ref first
const price = useSignalRef(priceSig);
watch(price, (newValue, oldValue) => {
console.log("price:", oldValue, "→", newValue);
});
Handling Component Remounts
During component remounts, leftover subscriptions can pose issues. Use component-scoped useComputedRef to automatically dispose of computations on unmount. Alternatively, manage the lifecycle with a Provider to explicitly handle disposal.
Example Solutions
-
Component-scoped:
const subtotal = useComputedRef(() => cartSig.get().items.reduce((sum, item) => sum + item.qty * item.price, 0) ); -
Provider-based:
export function provideStore() { const subtotal = coreComputed(/* ... */); provide(StoreKey, { subtotal }); onUnmounted(() => subtotal.dispose?.()); }
Strategies for Async Data
State-driven Rendering
Manage data lifecycle through the signal's effect system, allowing Vue to render different states like 'loading' or 'error'.
createEffect(() => {
const id = userId.get();
user.set({ status: "loading" });
fetch(`/api/user/${id}`)
.then((res) => res.json())
.then((data) => user.set({ status: "ok", data }))
.catch((err) => user.set({ status: "error", err }));
});
Suspense Resource
Wrap the model into a read() API to leverage Vue Suspense for async handling.
export function toResource<T>(src) {
let pending = null;
return {
read() {
const status = src.peek().status;
if (status === "ok") return src.peek().data;
if (status === "error") throw src.peek().err;
if (!pending) pending = new Promise(() => {});
throw pending;
}
};
}
Server-side Rendering Considerations
For SSR, ensure snapshot stability by using peek() for initial values. Start subscriptions only after mounting to avoid unnecessary server-side operations.
Example
export function useSignalRefSSR(src) {
const ref = shallowRef(src.peek());
let stop;
onMounted(() => {
stop = createEffect(() => {
ref.value = src.get();
});
});
onUnmounted(() => stop?.());
return ref;
}
Performance Optimization
Utilize equality comparisons in useComputedRef to minimize updates. For complex data types, apply strategies like shallowEqual.
const sorted = useComputedRef(
() => [...listSig.get()].sort((a, b) => a.id - b.id),
(a, b) => a.length === b.length && a.every((x, i) => x.id === b[i].id)
);
Common Pitfalls and Solutions
-
Incorrect
useComputedRefUsage:- Symptom: Loss of lazy evaluation.
- Fix: Use
signal.get()within computations.
-
Direct
.get()Calls in Templates:- Symptom: Non-reactive snapshots.
- Fix: Use
useSignalRefto convert to Vue refs.
-
Misuse of
watchEffect:- Symptom: Unpredictable reruns.
- Fix: Convert signals to Vue refs first.
-
Module-scoped Computed Values:
- Symptom: Uncleaned dependencies.
- Fix: Bind to component lifecycle or dispose manually.
Conclusion
Efficiently integrating signals in Vue requires a clear understanding of lifecycle management and reactive systems. By adhering to these best practices, you can maintain predictable updates and optimal performance.