Skip to content

What are Signals?

About 1294 wordsAbout 4 min

javascriptESNext

2024-03-25

In 2024, Signals is increasingly becoming the mainstream underlying technology dependence in the front-end framework. Probably which framework started to emerge, I have forgotten a little. The first one in my image to promote and popular was SolidJS. Later, this technology was also introduced in Vue3, followed by MobX, Preact, Qwik, Svelte, Angular, etc. This technology has also been introduced one after another.

In fact, the proposed Signals was much earlier than SolidJS, as early as 2010, Knockout has a similar implementation.

Therefore, Signals is not an "emerging, cutting-edge" front-end technology solution. On the contrary, the arguments, practices and applications of it are already quite mature. This is also the reason why Signals is becoming more and more popular with various frameworks nowadays.

Description

TC39 Added a Stage-0 Proposal. It is planned to integrate Signals into the JavaScript standard. And invited Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz Authors or contributors from multiple frameworks will participate in the discussion and jointly promote the implementation of the proposal.

I was quite excited to hear this news, which means that when Signals officially becomes the standard of ECMA, it will bring great convenience to our front-end development. Especially for major frameworks, using native Signals may lead to greater performance improvements, and even have the opportunity to share the same Signals between different frameworks. Bring more possibilities.

Why Signals?

In a common scenario, we need to implement a counter and want to render the value of the current counter to the page, Whenever the value of the counter changes, we re-render the page.

When we use native JavaScript implementation:

let counter = 0
const setCounter = (value) => {
  counter = value
  render()
}

const isEven = () => (counter & 1) == 0
const parity = () => (isEven() ? 'even' : 'odd')
const render = () => (element.innerText = parity())
// Simulate external updates to the counter...
setInterval(() => setCounter(counter + 1), 1000)

This seems to achieve the requirements, however, there are many problems here:

  • counter state is tightly coupled with the rendering system;
  • If counter changes, but parity does not change (such as counter changes from 2 to 4), Then unnecessary calculations and unnecessary rendering of parity will be performed;
  • What should I do if another part of the UI just wants to re-render when the counter changes?
  • What if another part of the UI depends only on isEven or parity?

Even in this relatively simple situation, many problems will soon arise. We can try to solve these issues by introducing Publish/Subscribe. This will allow other consumers of counter to subscribe to add their own reactions to state changes.

However, we may still have more problems:

  • The rendering function render only depends on parity, however, it actually requires a subscription to counter.
  • If you do not interact directly with counter, you cannot update the UI based solely on isEven and parity.
  • Introduced Publish/Subscribe, the problem is no longer just calling functions and reading variables, but subscribing and where to update, The issue of how to manage unsubscribe has also become complicated.

We resolve several issues including counter, isEven and parity by adding publish/subscribe. We must subscribe isEven to counter, subscribe parity to isEven, and subscribe render to parity. Then, this sample code is getting bigger and bigger, and we are stuck in a lot of subscriptions, if we don't have the right way to clean up the content, This may cause memory disasters. Although we solved some problems, we wrote a lot of code and introduced more problems.

Introducing Signals

Info

For ease of understanding, the following example uses the solid-js API, where:

  • [getter, setter] = createSignal(initial): Creates a signal, returning an array containing getter and setter.
  • createMemo(getter): Creates a read-only response value equal to the return value of the given function.
  • createEffect(effect): Create a side effect that is executed when its dependency changes.

To understand Signals, let's first make some modifications to the above example:

const [counter, setCounter] = createSignal(0)
const isEven = createMemo(() => counter() & (1 == 0))
const parity = createMemo(() => (isEven() ? 'even' : 'odd'))

createEffect(() => (element.innerText = parity()))

setInterval(() => setCounter(counter() + 1), 1000)

We can see immediately:

  • We eliminate unnecessary coupling of counter
  • Use a unified API to process values, calculate and side effects
  • The problem of no circular reference between counter and render
  • No manual subscription, no need to record dependencies
  • Can control the call timing of side effects

At the same time, Signals brings us more than just what we see on the surface:

  • Automatic dependency tracking: The calculated Signal will automatically discover any other Signal it depends on, whether these Signals are simple values ​​or other calculated values.
  • Delayed Evaluation: The calculation is not evaluated immediately upon declaration, nor is it reevaluated immediately upon dependency updates. They are evaluated only when the value is explicitly requested.
  • Memory: Calculate the signal caches its last value so that no matter how many times it is visited, there is no need to reevaluate the calculations that have not changed in its dependencies.

What are Signals?

Signals, i.e., a signal, which represents a data unit that may change over time. The signal can be "State" (just a manually set value) or "Computed" (a formula based on other signals). It is commonly known as Signals, or it can be called Observables, Atoms, Subjects, and Refs.

Signals is usually composed of getter, setter, value:

solid-js
const [count, setCount] = createSignal(0)

// Read value
console.log(count()) // 0

// Set value
setCount(5)
console.log(count()) //5

There seems to be nothing special about this, just a wrapper that can store values ​​of any type. But its focus is that getter and setter can run in any code, which is very helpful for updated publish and subscriptions.

Maybe you already know that in Vue, it is implemented using Object getters or Proxies:

vue
const count = ref(0)

// Read value
console.log(count.value) // 0

// Set value
count.value = 5
console.log(count.value) // 5

Or, like Svelte, hide behind the compiler:

svelte
let count = 0
// Read value
console.log(count) // 0

// Set value
count = 5

**In essence, signals are event transmitters. But the main difference lies in the way subscriptions are managed. **

Reactive

If other Reactives related to it are missing, it doesn't seem special to just look at Signals. Reactive, also known as Effects, Autoruns, Watches, or Computed. Reactive is run again when it updates occur by observing Signals.

console.log('1. Create Signal')
const [count, setCount] = createSignal(0)

console.log('2. Create Reaction')
createEffect(() => console.log('The count is', count()))

console.log('3. Set count to 5')
setCount(5)

console.log('4. Set count to 10')
setCount(10)

**Output: **

1. Create Signal

2. Create Reaction
The count is 0

3. Set count to 5
The count is 5

4. Set count to 10
The count is 10

This looks a bit magical, and the callback function of createEffect was executed immediately after setCount(). This doesn't seem to be directly related, but in fact, in the createEffect callback function, it is automatically executed when count() Subscribed to the createEffect callback function, and the createEffect callback function is re-run whenever count changes.

How to implement Signals?

TODO...