Signal
Signals are lightweight observable pattern objects (similar to Stores), that can be defined inside or outside of kaioken components.
Example case 0
import { signal } from "kaioken"
const Counter = () => {
const count = signal(0)
const increment = () => count.value++
const decrement = () => count.value--
return (
<div className="flex gap-4">
<button onclick={decrement}>-</button>
<p>{count.value}</p>
<button onclick={increment}>+</button>
</div>
)
}
You can see here that count is a signal that contains a value property. Under the hood, the value propery has a getter and setter.
Invoking the getter will return the current value of the signal, and if done during the rendering phase, the component will be subscribed.
Invoking the setter will immediately update the value of the signal and trigger a re-render for all components that are subscribed.
toString?
A simple ease of life for signals is we override the toString method on the signal itself to always be the toString method of the value,
this can nice for when using this value in jsx, for example in our case 0 example, we could update count.value to just be count.
const Counter = () => {
const count = signal(0)
const increment = () => count.value++
const decrement = () => count.value--
return (
<div className="flex gap-4">
<button onclick={decrement}>-</button>
<p>{count.value}</p>
<p>{count}</p>
<button onclick={increment}>+</button>
</div>
)
}
Signals can be created and used anywhere
What does this mean? Well, let's up the complexity a bit from a counter with a tiny todo app.
// signals.js
const input = signal("")
const todos = signal([])
// Todos.jsx
import { input, todos } from "signals.ts"
const Page = () => {
const handleSubmit = (e) = > {
e.preventDefault();
todos.value = [...todos.value, input.value]
input.value = ""
}
return (
<form onsubmit={handleSubmit}>
<input type="text" oninput={(e) => (input.value = e.target.value)} />
<ul>
{todos.value.map((item) => <li>{item}</li>)}
</ul>
</form>
)
}
You can see here even though, todos & input is defined inside of signals.ts, we can import them inside a single or multiple components and still keep our reactivity!
Quick gotcha!
You can not destructure signals else it will loose it's reactiviesness, ie
const count = signal(0)
const { value } = count; // value is no longer reactive!
What if you want to listen to value updates outside of the kaioken context.
We've got you covered! All signals have a subscribe method that will fire anytime the value updates.
const count = signal(0)
const stop = count.subscribe(($count) => {
console.log(`count is now ${$count}`)
// any business logic you'd like you add!
})
subscribe returns it's unsubscribe method allowing you stop listening to any updates when ever you please!
Signals only updates the components that care!
Let's say we have a parent & child situation, like so...
const Child = ({ element }) => {
console.log("Child render")
return element()
}
const Parent = () => {
const count = signal(0);
const increment = () => count.value++
console.log("Parent render")
return (
<Child
element={() => <button onclick={increment}>{count}</button>}
/>
)
}
When mutating count, signals are smart enough to only add Child to the internal list of 'subscribed nodes', meaning anytime it updates it will only re render Child even though it's defined in Parent.
One more tiny thing
You may have noticed in our todo app example this line
todos.value = [...todos.value, input.value]
Why not just todos.value.push(input.value)? This is because signals only react to explicit sets on the value properly! This means you cannot deeply mutate the value and expect it to react. However, we supply a special notify method that allows you to notify all the subscribers (including components that have automatically subscribed to the signal), after you deeply mutate the value, so we can update the todos onSubmit method to look like so.
const onSubmit = (e) => {
e.preventDefault()
todos.value.push(input.value)
input.value = ""
todos.notify() // emits a signal change
}