Tools
Tools: Two JavaScript Fundamentals You Need Before Implementing Signals
2026-01-21
0 views
admin
Why This Article Exists ## Closures ## Why This Pattern Works Well ## Encapsulation (Immutability by intent) ## Stable references ## Compared to useState ## Destructuring Assignment ## Array Destructuring ## Object Destructuring ## Common Pitfalls ## Destructuring Extracts References, Not Copies ## Don’t Cache Values If You Need Fresh State ## Why Signals Prefer “Closure + Destructuring” ## Frequently Asked Questions ## Can I do const value = get() and pass it around? ## Does destructuring break reactivity? ## Why not use a class? ## A Signal That Returns an Object ## Conclusion 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: If you’re not comfortable with these two concepts, it’s very easy to fall into common misunderstandings during tutorials—such as: 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.) 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: 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. 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. 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. 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. Let’s use Solid’s createSignal as an example: Using our earlier signal example: Renaming is also supported: What you receive is a function reference, not a snapshot of the value.
The current state is only read when you actually call get(). If you want the latest value, call get() again—don’t reuse an old variable. 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. Yes, but that value is a snapshot.
If you need live state, pass the get function itself or call get() at the usage site. No. Reactivity occurs at the read point—when get() is called—not when the function is destructured. Closures are lighter, avoid this binding issues, and work better with tree-shaking and functional composition. Combining everything above leads to the following foundational API, which we’ll extend in later articles: 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. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
const { get, set } = signal(0); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const { get, set } = signal(0); CODE_BLOCK:
const { get, set } = signal(0); COMMAND_BLOCK:
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 COMMAND_BLOCK:
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 COMMAND_BLOCK:
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 COMMAND_BLOCK:
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 COMMAND_BLOCK:
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); COMMAND_BLOCK:
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); COMMAND_BLOCK:
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 COMMAND_BLOCK:
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); COMMAND_BLOCK:
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); CODE_BLOCK:
const { get: count, set: setCount } = signal(0);
setCount(count() + 1); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const { get: count, set: setCount } = signal(0);
setCount(count() + 1); CODE_BLOCK:
const { get: count, set: setCount } = signal(0);
setCount(count() + 1); CODE_BLOCK:
const { get } = signal(0); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const { get } = signal(0); CODE_BLOCK:
const { get } = signal(0); CODE_BLOCK:
const { get, set } = signal(0); const v = get(); // snapshot
set(10); console.log(v); // still 0, not 10 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const { get, set } = signal(0); const v = get(); // snapshot
set(10); console.log(v); // still 0, not 10 CODE_BLOCK:
const { get, set } = signal(0); const v = get(); // snapshot
set(10); console.log(v); // still 0, not 10 COMMAND_BLOCK:
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 COMMAND_BLOCK:
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 };
} COMMAND_BLOCK:
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 };
} - thinking values are “snapshotted” and reactivity is broken, or
- running into incorrect assumptions about this binding. - 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.
how-totutorialguidedev.toaijavascript