Signal
A Signal is a reactive primitive used to manage state. Signals hold a value that can be read and updated. When the value changes, any subscribers to the signal are notified, allowing for efficient updates to dependent components.
Creating a Signal
import { signal } from "kaioken"
const userName = signal("bob")
signal
takes an initial value and optionally a displayName
for debugging. It 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
- 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()
Going forward, we'll refer to the act of reading a signal's value as observing. 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.
Use computed
to create a signal derived from a getter function:
import { computed } from "kaioken"
const userGreeting = computed(() => `Hello, ${userName}!`)
The computed
signal will automatically track dependencies (signals that were observed
)
and update whenever any of them change.
We've also snuck a quality-of-life feature in here - signals
implement toString()
so they can be used in strings!
Use watch
create a that
will fire the callback whenever observed
signals change.
import { watch } from "kaioken"
const watcher = watch(() => console.log(userGreeting.value))
watcher.stop()
watcher.start()
The callback provided to watch
will fire immediately.
When a signal that it observes
changes, it will be queued to fire again
within a microtask.
This allows us to automatically "batch" execution of callbacks.
General usage
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>
)
}
In the above example, our userGreeting
and userName
signals from earlier
are observed
by the component during render, causing the component to
automatically subscribe to them. This means the component and all of its
children will be updated whenever their values 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 neither of the signals are observed
at the time of rendering,
when they change, Kaioken will only change the things that matter - in
this case, the text node inside of our heading that displays the greeting
and the value
attribute of our input.
Using local signals
signal
, computed
, and watch
can all be created locally in Kaioken
components via the useSignal
, useComputed
, and useWatch
hooks.
This will allow them to be persisted across renders and automatically
disposed of when the component unmounts.
import { useSignal, useComputed, useWatch } from "kaioken"
function App() {
const userName = useSignal("bob")
const userGreeting = useComputed(() => `Hello, ${userName}!`)
useWatch(() => console.log(userName.value))
return (
<div>
<h1>{userGreeting}</h1>
<input
type="text"
value={userName}
oninput={(e) => userName.value = e.target.value}
/>
</div>
)
}
The bind:
prefix can be used to create two-way binding for any property
that changes via user interaction. When the signal changes, the property
is updated, and vice versa.
function App() {
const userName = useSignal("bob")
useWatch(() => console.log(userName.value))
return (
<div>
<input type="text" bind:value={userName} />
</div>
)
}
The <For />
component iterates over a signal, producing an automatically-updating
list with fine-grained reactivity. This is a great optimization tool for when you would otherwise
use signal.value.map((item) => ...)
in JSX, causing the component that renders
it to update whenever the signal changes.
function App() {
const items = useSignal([0, 1, 2, 3, 4])
const doubledItems = useComputed(() => items.value.map((i) => i * 2))
const addItem = () => (items.value = [...items.value, items.value.length])
return (
<div>
<ul>
<For each={doubledItems}>{(item) => <li>{item}</li>}</For>
</ul>
<button onclick={() => (items.value = [...items.value, items.value.length])}>
Add
</button>
</div>
)
}
Similar to <For />
, <Derive />
allows you to easily create fine-grained reactivity in JSX.
The <Derive />
component produces a JSX element that automatically updates whenever the
provided from
signal changes.
function App() {
const name = useSignal("bob")
const age = useSignal(42)
const person = useComputed(() => ({ name: name.value, age: age.value }))
return (
<div>
<input bind:value={name} />
<input type="number" bind:value={age} />
<Derive from={person}>
{(person) => (
<div>
{person.name} is {person.age} years old
</div>
)}
</Derive>
{/* You can also use multiple signals! */}
<Derive from={[name, age]}>
{(name, age) => (
<div>
{name} is {age} years old
</div>
)}
</Derive>
</div>
)
}