Signal

A Signal is a reactive primitive used to manage state. Signals hold a value that can be observed and updated. When the value changes, any subscribers to the signal are notified, allowing for efficient updates to dependent components.

General Usage


Creating a Signal

import { signal } from "kaioken"

const userName = signal("bob")

The signal function takes an initial value and optionally a displayName for debugging. This returns an instance of the Signal class with the following properties:

  • value: Gets or sets the signal's current value
  • subscribe: Registers a function to be called when the signal's value changes
  • map: Derives a new signal based on the current signal's value
  • peek: Retrieves the value without tracking reactivity
  • sneak: Sets the value without emitting a signal change
  • notify: Emits a signal change

Reading and Writing Signal Values

You can access a signal's value directly:

console.log(userName.value)  // "bob"
userName.value = "alice"

Assigning a new value triggers reactivity, notifying any subscribers. If you mutate the signal's value without assigning to the value property, eg. mySignal.value.something = "test", the signal will not notify subscribers. In this case, use mySignal.notify() to manually trigger an update.

Subscribing to Signals

You can subscribe to a signal's value changes using the subscribe method:

const unsubscribe = userName.subscribe((newValue) => {
  console.log("Value updated:", newValue)
})

The subscribe function returns an unsubscribe function, allowing you to remove the subscription when needed:

unsubscribe()

Derived Signals


Derived signals are read-only, meaning their value cannot be set directly.

Use signal.map() to create a derived signal from another signal:

const userAge = signal(25)
const userJoke =  userAge.map((age) => 
  `${userAge} walked into a bar... 
  and the bartender said, 'Why the long integer?'`
)

Use computed to create a derived signal from a getter function:

import { computed } from "kaioken"

const userGreeting = computed(() => `Hello, ${userName}! ${userJoke}`)

The computed signal will automatically track dependencies and update whenever the tracked signals change.

We've snuck a quality-of-life feature in here - signals implement toString() and therefore are able to be used in strings.

Signals in Components


In Kaioken components, reading and writing signals is slightly nuanced but has the capability to provide unmatched performance.

function App() {
  return (
    <div>
      <h1>{userGreeting.value}</h1>
      <input 
        type="text" 
        value={userName.value} 
        oninput={(e) => userName.value = e.target.value} 
      />
    </div>
  )
}

Here, userGreeting.value and userName.value are read directly inside the component during render, causing the component to automatically subscribe to them. This means the component will re-render whenever userName.value or userGreeting.value change.

While this may be the desired effect, signals can be much more performant when used for text or attributes. See the following:

function App() {
  return (
    <div>
      <h1>{userGreeting}</h1>
      <input 
        type="text" 
        value={userName} 
        oninput={(e) => userName.value = e.target.value} 
      />
    </div>
  )
}

Because the signal's value is not read at the time of rendering, when it changes, Kaioken will only change the things that matter - in this case, the text node that displays the greeting and the value attribute.

This is one of the more complex, but powerful aspects of signals. The core philosophy around their design is reactivity via observation, where it matters.