Skip to content

📡 Signals for Fine-Grained Reactivity ​

thing

Before we dive into Re4, let’s understand the core primitive that powers its reactivity system - built around signals.

State is what makes UIs dynamic. Whether it’s a counter, a form, or an interactive canvas full of moving shapes, the app needs to react when values change. Different frameworks solve this with different tools - stores, proxies, observables - each with tradeoffs.

Signals offer a simpler alternative.
They’re lightweight, precise, and designed for fine-grained updates: only the parts of your code that use a signal will re-run when it changes.


What Is a Signal? ​

A signal is a wrapper around a value that can track which code is using it and notify that code when the value changes.

Here’s a basic example:

ts
const count = signal(1);

console.log(count.value); // 1

count.value = 2;

console.log(count.value); // 2

It may look like a getter/setter, but signals also remember which parts of your app used count.value. That’s where the magic begins.


Reacting to Changes with effect ​

To respond when a signal changes, wrap your logic in an effect.

ts
effect(() => {
  console.log('Count is:', count.value);
});

Now, whenever count.value changes, this effect re-runs:

ts
count.value = 3;
// logs: Count is: 3

No need to manually subscribe - signals track usage automatically when accessed inside an effect.

This makes it easy in situations where you want to update just a part of the DOM based on a signal. For example:

typescript
const count = signal(0);

const el = document.querySelector('#countDisplay');

effect(() => {
  el.textContent = `Count is: ${count.value}`; // auto-tracked dependency
});

setInterval(() => {
  count.value += 1; // trigger update every second
}, 1000);

This code:

  • Gets the display element
  • Uses effect() to update the text whenever count.value changes.
  • Updates the signal every second, which re-runs the effect and updates the DOM

Derived Values with computed ​

Let’s say you want to calculate a value based on other signals - for example,
total = price × quantity. You don’t want to update total manually every time.

That’s what computed is for:

ts
const price = signal(10);
const quantity = signal(2);

const total = computed(() => price.value * quantity.value);

console.log(total.value); // 20

quantity.value = 3;
console.log(total.value); // 30

total updates automatically when price or quantity changes. And it’s read-only - it always reflects the latest derived value.


Let’s Write a Mini Signals Runtime from Scratch ​

Step 1: The signal function ​

We create a signal by wrapping a value and tracking who depends on it.

ts
let currentEffect = null;

function signal(initialValue) {
  let value = initialValue;
  const subscribers = new Set();

  return {
    get value() {
      if (currentEffect?.firstRun) {
        subscribers.add(currentEffect.run);
      }
      return value;
    },
    set value(newValue) {
      if (newValue !== value) {
        value = newValue;
        for (const run of subscribers) run();
      }
    },
  };
}

Step 2: The effect function ​

This runs a function and records which signals it accessed. Those signals can then notify this function when they change.

ts
function effect(fn) {
  const context = {
    firstRun: true,
    run: () => {
      currentEffect = context;
      fn();
      currentEffect = null;
      context.firstRun = false;
    },
  };
  context.run();
}

Try it out: ​

typescript
const a = signal(1);
const b = signal(2);

effect(() => {
  console.log('a + b =', a.value + b.value);
});

a.value = 5; // re-runs effect
b.value = 7; // re-runs again

Output:

a + b = 3
a + b = 7
a + b = 12

Since we’re here, let’s also implement a computed function - for values that depend on other signals but update automatically.

The computed function ​

computed() works just like effect(), but instead of running side effects, it returns a read-only signal that tracks its dependencies and recalculates when they change.

ts
function computed(fn) {
  const result = signal();
  effect(() => {
    result.value = fn();
  });
  return {
    get value() {
      return result.value;
    },
  };
}

Try it out: ​

ts
const price = signal(50);
const quantity = signal(2);

const total = computed(() => price.value * quantity.value);

console.log(total.value); // 100

quantity.value = 3;
console.log(total.value); // 150

This lets you build derived reactive values that stay up-to-date without any extra wiring.

This is the core idea:

  • A value that tracks who’s watching
  • Notifies them when it changes

Real libraries optimize this with batching, better data structures, and scheduling - but this example gives you the mental model.


Signals vs Observables ​

You might wonder - are signals like observables or event emitters?

Not quite:

AspectSignalsObservables
Push/PullPull-based (you read)Push-based (you subscribe)
TrackingAutomatic on accessManual .subscribe()
TimingAlways synchronousCan be synchronous or asynchronous

Signals fit better in declarative UIs where logic depends on data, and the system figures out when to re-run that logic.


TL;DR ​

  • A signal wraps a value and re-runs any code that depends on it when it changes.
  • Use effect() to run code that reacts to signal changes automatically.
  • Use computed() for values derived from other signals - they’re read-only and stay in sync.
  • Signals are synchronous, auto-tracked, and minimal.
  • Perfect for building fast, fine-grained reactivity with little runtime overhead.

🚀 Up Next ​

How Re4 uses signals under the hood (coming soon!).

Resources & Credits ​



Like Re4? Follow me on X for more updates:

— Aadi (Follow on X)

Copyright © 2025-present ८४ Labs.