news Apr 16, 2026 · 5 views · 3 min read

Integrating Signals with Vue: A Composition API Guide

Explore how to seamlessly integrate signals with Vue's Composition API, ensuring efficient data binding and smooth UI rendering without double dependency tracking.

Integrating Signals with Vue's Composition API

In this article, we delve into the integration of our signal system within Vue's environment, focusing on maintaining the integrity of our reactive graph while utilizing the Vue 3 Composition API.

Article Objective

The primary goal is to connect our signal and computed primitives to Vue's Composition API. This connection should allow direct template usage while maintaining the behavior of our reactive graph, including:

  • Push dirty-marking
  • Pull recomputation
  • Avoiding double dependency tracking
  • Preventing scheduler conflicts

Core Design Principles

One-Way Bridge

Synchronize only the value into a Vue ref, avoiding feeding Vue's reactivity back into your graph. This prevents circular scheduling problems.

Clear Lifecycle Management

Ensure cleanup by calling createEffect and disposing with computed.dispose() inside onUnmounted.

Snapshot Source

  • Use peek() for initialization, which avoids tracking and allows lazy recomputation.
  • Use get() within effects to establish dependencies.

Consistent Mental Model

Callbacks passed to useComputedRef must read signal.get() to establish dependencies. Vue's computed should be used for pure Vue computations.

Who Depends on Whom (Vue Version)

  • Templates and watch functions observe Vue refs via useSignalRef.
  • Computed values read signal.get() to establish dependencies.

Implementing the Adapter

import { shallowRef, onUnmounted } from "vue";
import { createEffect } from "../core/effect.js";
import { computed as coreComputed } from "../core/computed.js";

export function useSignalRef(src) {
  const r = shallowRef(src.peek());
  const stop = createEffect(() => {
    r.value = src.get();
  });
  onUnmounted(() => stop());
  return r;
}

export function useComputedRef(fn, equals = Object.is) {
  const memo = coreComputed(fn, equals);
  const r = useSignalRef({
    get: () => memo.get(),
    peek: () => memo.peek(),
  });
  onUnmounted(() => memo.dispose?.());
  return r;
}

Why Use shallowRef?

Equality checks and caching are handled by the core. Vue only needs to detect value changes, leaving deep tracking to core strategies.

Update and Cleanup Timing

  • Use peek() for initial snapshots.
  • Use get() within effects for dependency establishment.
  • onUnmounted → stop() ensures no lingering subscriptions.

Usage Examples

Counter: Signal + Derived Value

<script setup lang="ts">
import { signal } from "../core/signal.js";
import { useSignalRef, useComputedRef } from "./vue-adapter";

const countSig = signal(0);
const count = useSignalRef(countSig);
const doubled = useComputedRef(() => countSig.get() * 2);
const inc = () => countSig.set(v => v + 1);
</script>

<template>
  <p>{{ count }} / {{ doubled }}</p>
  <button @click="inc">+1</button>
</template>

Selector: Observing Part of an Object

<script setup lang="ts">
import { signal } from "../core/signal.js";
import { useComputedRef } from "./vue-adapter";

const userSig = signal({ id: 1, name: "Ada", age: 37 });
const nameRef = useComputedRef(() => userSig.get().name, (a, b) => a === b);
</script>

<template>
  <h2>{{ nameRef }}</h2>
</template>

Scoping Computed Values

  • Component Scope: Use useComputedRef, which auto-disposes on unmount.
  • Module Scope: Manually call dispose when a global computed is no longer needed.

Interoperability with watch / watchEffect

Convert signals or computed values to a Vue ref using useSignalRef before observing them with watch or watchEffect.

Responsibility Boundaries

  • Data/Business Logic: Managed by createEffect.
  • UI/DOM Logic: Managed by Vue lifecycle hooks.

Common Pitfalls

  • Avoid reading signal.get() directly inside templates or setup.
  • Define a single source of truth for data, typically signals, to prevent circular scheduling.

Conclusion

This guide provides a structured approach to integrating signals with Vue's Composition API. By adhering to these principles, you can ensure smooth data binding and UI rendering, avoiding issues like double dependency tracking and scheduler conflicts. In our next installment, we will explore advanced interoperability scenarios.

Discussion

0 Comments

Leave a Comment

Comments are moderated and will appear after approval.