A glowing reactive dependency graph on a developer's desk with a gray-blue British shorthair cat resting nearby.

Alien Signals: The Tiny Reactivity Core That Powers Vue

The Gray Cat
The Gray Cat

Every modern frontend framework has converged on the same idea: instead of re-running your whole UI when something changes, track exactly which values depend on each other and update only what actually moved. That idea is called fine-grained reactivity, and Alien Signals is one of the smallest, fastest, and most influential implementations of it. The package alien-signals is a dependency-free TypeScript library that gives you signal, computed, and effect primitives backed by a hand-tuned push-pull algorithm.

What makes it worth your attention is the pedigree. The library started life as the author's experiment to speed up Vue 3.4's reactivity, kept evolving as a research project, and ended up so fast that its core algorithm was ported directly into Vue 3.6's production reactivity engine. The same core now also powers XState and Vue Language Tools (Volar). In other words, this tiny package is battle-tested at a scale most state libraries never reach. It is not a framework — there is no JSX, no DOM binding, no components. It is the reactivity engine on its own, ready to drop into anything from a state-machine library to a React store you read through useSyncExternalStore.

Why Reach for It

A few qualities set alien-signals apart from the crowded field of state tools:

  • It is genuinely tiny. A single source file, zero runtime dependencies, roughly one to two kilobytes minified and gzipped. It markets itself as "the lightest signal library" and earns the title.
  • It is extremely fast. Microbenchmarks report around 4x the throughput of Vue 3.4's reactivity, and over 30x faster than Vue 3.5 in scenarios that read many computeds after a change. The speed comes from a deliberate design: no Array, Set, or Map in the graph traversal, no recursion, and bitwise state flags instead of allocated objects.
  • It is correct, not just quick. Version 3.2.0 added a conformance test suite and passes all 180 cases, including glitch-free handling of the classic diamond dependency problem.
  • It is extensible. A lower-level createReactiveSystem factory exposes the primitive graph operations so framework authors can build their own reactive APIs on the same optimized core. This is exactly the seam Vue and XState build on.
  • It is portable. The algorithm has been re-implemented in Dart, Lua, Java, C#, Go, and Rust, which is strong evidence the design is small and language-agnostic.

Getting It Installed

Add it to your project with whichever package manager you prefer:

npm install alien-signals
yarn add alien-signals

There are no peer dependencies to worry about and the entire public API is tree-shakeable, so an app that only touches the high-level primitives never pulls in the low-level machinery.

Your First Three Primitives

The whole everyday surface is three functions. Values are both read and written through a single overloaded function, the same style SolidJS and Preact Signals popularized: call it with no arguments to read, call it with an argument to write.

import { signal, computed, effect } from 'alien-signals';

const count = signal(1);
const doubleCount = computed(() => count() * 2);

effect(() => {
  console.log(`Count is: ${count()}`);
}); // Console: Count is: 1

console.log(doubleCount()); // 2

count(2); // Console: Count is: 2

console.log(doubleCount()); // 4

There is a lot happening in those few lines. signal(1) creates a reactive container. computed derives a memoized value from it, and crucially that derivation is lazy: doubleCount does not recompute the moment count changes, only when you actually read it and a dependency genuinely moved. effect runs its callback once immediately, records every signal read during that run, and re-runs automatically whenever any of those dependencies change. You never declare dependency arrays by hand the way you would with a React hook — the tracking is automatic and exact.

Cleaning Up After Yourself

As of version 3.2.0, effects gained an ergonomic feature that React developers will recognize instantly: an effect callback can return a cleanup function. That function runs before each re-execution and again when the effect is disposed, which is perfect for unsubscribing from things, clearing timers, or tearing down listeners.

import { signal, effect } from 'alien-signals';

const userId = signal(1);

effect(() => {
  const id = userId();
  const socket = openSocket(`/users/${id}`);

  return () => {
    socket.close(); // runs before the next run and on disposal
  };
});

userId(2); // closes the old socket, then opens a new one

This single addition closes most of the gap between an alien-signals effect and a useEffect mental model, while keeping the dependency tracking automatic instead of manual.

Grouping Effects Into Scopes

Real applications create effects in clusters — a component mounts, a feature panel opens, a route loads — and those clusters need to be torn down together. effectScope groups every effect created inside it and hands you a single stop function that disposes all of them at once. Since version 2 it also disposes nested child scopes recursively.

import { signal, effect, effectScope } from 'alien-signals';

const count = signal(1);

const stopScope = effectScope(() => {
  effect(() => {
    console.log(`Count in scope: ${count()}`);
  }); // Console: Count in scope: 1
});

count(2); // Console: Count in scope: 2

stopScope();

count(3); // No console output — the scope is disposed

This is the cleanup primitive you reach for when wiring alien-signals into a component lifecycle. Create a scope when the component mounts, call its stop when it unmounts, and every effect inside is guaranteed to be released without leaks.

The One Gotcha: In-Place Mutation

Here is the footgun every newcomer hits, so it is worth meeting it head-on. Because signals track values by identity, mutating an array or object in place does not change the reference, so subscribers never fire. alien-signals deliberately does not use proxies to catch this (that is exactly the kind of overhead it avoids for the sake of speed). Instead it gives you trigger for manual invalidation.

import { signal, computed, trigger } from 'alien-signals';

const arr = signal<number[]>([]);
const length = computed(() => arr().length);

console.log(length()); // 0

arr().push(1);
console.log(length()); // Still 0 — the mutation bypassed the setter

trigger(arr);
console.log(length()); // 1

The rule of thumb is simple: if you replace the whole value (arr([...arr(), 1])) reactivity just works, but if you mutate in place you must call trigger. The trigger function also accepts a callback form so you can batch several manual mutations into one invalidation. If you would rather not think about this at all, ecosystem wrappers like alien-deepsignals and @sigrea/core add proxy-based deep reactivity on top of the core.

Building Your Own Primitives

The high-level API is intentionally minimal, but underneath it sits createReactiveSystem, a factory that returns the raw graph operations — link, unlink, propagate, and checkDirty, along with update and notify hooks. This is the seam that lets framework authors invent their own reactive semantics while reusing the same optimized engine. It is how Vue's reactivity and XState's atom architecture are built on the very same core you just used.

import { createReactiveSystem } from 'alien-signals';

const { link, unlink, propagate, checkDirty } = createReactiveSystem({
  update(node) {
    // recompute a derived node, return true if its value changed
    return runComputation(node);
  },
  notify(effectNode) {
    // schedule an effect to run
    scheduleEffect(effectNode);
  },
  // ...additional lifecycle hooks
});

You will not need this for application code — reach for it only when you are designing a new reactive abstraction. And a fair warning from the maintenance history: while the high-level signal, computed, and effect API has stayed remarkably stable across major versions, the low-level surface has seen renames between v1, v2, and v3. If you build on createReactiveSystem, pin your version and read the changelog before upgrading.

How the Magic Works

The reason alien-signals is fast is worth understanding even if you never touch the internals, because it explains its behavior. The engine is a push-pull hybrid. On the push side, writing a signal eagerly propagates through the dependency graph, but it only marks downstream nodes as needing a check rather than recomputing them. On the pull side, computeds stay lazy and recompute only when read, and only after a checkDirty walk confirms an upstream value truly changed. That combination means work is never wasted on derived values nobody is looking at.

Three deliberate constraints make this cheap. The graph uses doubly-linked Link nodes instead of arrays, sets, or maps, which eliminates allocation churn and garbage-collection pressure during frequent updates. Node state lives in compact bitwise flags — Dirty, Pending, Recursed, and friends — rather than separate boolean fields. And there is no recursion anywhere in the core; propagate and checkDirty walk the graph with explicit stack iteration, which sidesteps call-stack overhead and stack-overflow risk in deep graphs. The Pending-then-Dirty flag scheme is also what gives you glitch-free updates: when a value can reach a node through multiple paths (the diamond problem), that node is evaluated exactly once, after all its inputs settle, so your effects never observe an inconsistent intermediate state.

Where React Fits In

It is worth being honest with React developers. React has no native fine-grained reactivity — it re-renders components and reconciles, which is the opposite philosophy to signals. So alien-signals does not magically make React update surgically. What it does give you is an excellent engine for non-component state and framework-agnostic stores, which you then bridge into React through useSyncExternalStore, the React 18+ API designed precisely for subscribing to external stores. Community bindings like react-alien-signals and reactjs-signal package that bridge for you, and the WebReflection fork even exposes a Preact-style .value API on top of the same core if that feels more natural coming from Preact or React.

The deeper value for a React audience may simply be the mental model. Once you have written a few effects with automatic dependency tracking and watched lazy computeds skip work nobody requested, the difference between fine-grained reactivity and coarse component re-rendering stops being abstract.

The Takeaway

alien-signals is a rare thing: a library small enough to read in an afternoon and serious enough to power one of the largest frontend frameworks in the world. Its three-function API is friendly, its cleanup story with returned functions and effect scopes is solid, and its push-pull engine is both fast and provably correct. The main thing to remember is the in-place mutation rule and trigger; everything else just tracks dependencies for you. Whether you want a tiny standalone store, a foundation for your own reactive abstraction, or just a clear window into how modern reactivity actually works, this alien is very much worth signaling.