DEV Community

Cover image for Two JavaScript Fundamentals You Need Before Implementing Signals
Luciano0322
Luciano0322

Posted on

Two JavaScript Fundamentals You Need Before Implementing Signals

Why This Article Exists

In the upcoming articles, we’ll implement signal() using closures to preserve state, and we’ll read and write values via object destructuring, for example:

const { get, set } = signal(0);
Enter fullscreen mode Exit fullscreen mode

If you’re not comfortable with these two concepts, it’s very easy to fall into common misunderstandings during tutorials—such as:

  • thinking values are “snapshotted” and reactivity is broken, or
  • running into incorrect assumptions about this binding.

This is not hypothetical. In a previous role at a well-known company, I encountered a frontend tech lead who had a flawed understanding of destructuring assignment—likely due to limited exposure outside of React.

So, just to be safe, let’s briefly review these fundamentals before moving on.
(If you already understand them well, feel free to skip this article.)


Closures

A closure allows a function to remember variables from the lexical scope in which it was created—even after that scope has finished executing.

If you want a deeper explanation, you can refer to my Medium article on closures. If lexical scope itself is unclear, I recommend reviewing that first.

Here, we’ll adapt a standard closure example into what a basic signal implementation looks like:

function signal<T>(initial: T) {
  // Private state remembered by the closure
  let value = initial;

  // Read
  const get = () => value;

  // Write
  const set = (next: T) => {
    value = next;
  };

  // Returning an object is more readable for most people;
  // an array form is also possible if you prefer.
  return { get, set };
}

const count = signal(0);
count.set(count.get() + 1);
console.log(count.get()); // 1
Enter fullscreen mode Exit fullscreen mode

Why This Pattern Works Well

Encapsulation (Immutability by intent)

The external world cannot access value directly—only via get and set.
This is the same design intent as private fields in classes or Proxy-based encapsulation.

Stable references

get and set are stable function references; they do not need to be recreated.
This makes them ideal for integration with React, event handlers, or any callback-based system.

Compared to useState

React hooks must be called during render.
A closure-based signal() can be created at any time and is framework-agnostic.

Closures are not magic—they’re just functions + lexical scope.
Once you internalize this, topics like dependency tracking and schedulers will feel much more natural.


Destructuring Assignment

Arrays or objects can be “unpacked” into multiple variables, with support for renaming, default values, and nested patterns.

This is the exact concept I’ve seen misunderstood in real-world teams. If you want a more comprehensive explanation, you can refer to a dedicated destructuring article.

Array Destructuring

Let’s use Solid’s createSignal as an example:

function createSignal<T>(initial: T) {
  let value = initial;
  const getter = () => value;
  const setter = (next: T) => {
    value = next;
  };
  return [getter, setter] as const;
}

const [count, setCount] = createSignal(0);
setCount(count() + 1);
Enter fullscreen mode Exit fullscreen mode

Object Destructuring

Using our earlier signal example:

function signal<T>(initial: T) {
  let value = initial;
  const get = () => value;
  const set = (next: T) => {
    value = next;
  };
  return { get, set };
}

const { get, set } = signal(0);
set(get() + 1);
Enter fullscreen mode Exit fullscreen mode

Renaming is also supported:

const { get: count, set: setCount } = signal(0);
setCount(count() + 1);
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

Destructuring Extracts References, Not Copies

const { get } = signal(0);
Enter fullscreen mode Exit fullscreen mode

What you receive is a function reference, not a snapshot of the value.
The current state is only read when you actually call get().

Don’t Cache Values If You Need Fresh State

If you want the latest value, call get() again—don’t reuse an old variable.

const { get, set } = signal(0);

const v = get(); // snapshot
set(10);

console.log(v); // still 0, not 10

Enter fullscreen mode Exit fullscreen mode

Why Signals Prefer “Closure + Destructuring”

  • Closures keep internal state private and controlled, and naturally support
    extensions like computed caching, equality checks, and subscriber lists.

  • Object destructuring produces clearer APIs (get, set, peek, on, etc.)
    compared to position-sensitive array destructuring, and scales better as an
    API becomes more discoverable.


Frequently Asked Questions

Can I do const value = get() and pass it around?

Yes, but that value is a snapshot.
If you need live state, pass the get function itself or call get() at the usage site.

Does destructuring break reactivity?

No. Reactivity occurs at the read point—when get() is called—not when the function is destructured.

Why not use a class?

Closures are lighter, avoid this binding issues, and work better with tree-shaking and functional composition.


A Signal That Returns an Object

Combining everything above leads to the following foundational API, which we’ll extend in later articles:

export type Signal<T> = {
  get(): T;
  set(next: T | ((prev: T) => T)): void;
};

export function signal<T>(initial: T): Signal<T> {
  let value = initial;

  const get = () => value;

  const set = (next: T | ((prev: T) => T)) => {
    const nextValue =
      typeof next === "function"
        ? (next as (p: T) => T)(value)
        : next;

    if (!Object.is(value, nextValue)) {
      value = nextValue;
    }
  };

  return { get, set };
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This article reviewed two fundamental JavaScript concepts—closures and destructuring—and showed how they form the foundation of a Signal implementation.

In the next article, we’ll build on this foundation by introducing a subscription mechanism.

Top comments (0)