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 "kiru"
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 ComputedSignal, which is derived from a getter function.
A ComputedSignal is lazily evaluated, meaning that the getter function won't be called
until it is observed or subscribed to. Manually assigning a new value to it has no effect.
import { computed } from "kiru"
const userGreeting = computed(() => `Hello, ${userName}!`)The computed signal will automatically track dependencies (signals that were observed)
and update whenever any of them change.
Computed signals also receive the previous value as an argument to the getter function. This can be useful for cleaning up previous resources, or for creating a new resource based on the previous value.
const websocket = computed<WebSocket>((prev) => {
prev?.close()
// ^ Websocket | undefined
return new WebSocket(`ws://localhost:3000/rooms/${userName}`)
})We've also snuck a quality-of-life feature in here - signals
implement toString() so they can be used in strings!
Use effect create a that
will fire the callback whenever observed signals change.
import { effect } from "kiru"
const effectHandle = effect(() => console.log(`Greeting has been updated: ${userGreeting}`))
effectHandle.stop()
effectHandle.start()The callback provided to effect 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.
You can also explicitly pass a list of signals to effect to observe.
Their values will be passed to the callback in the order they were passed.
const handle = effect([userName, userGreeting], (newName, newGreeting) => {
console.log(`Name has been updated: ${newName}`)
console.log(`Greeting has been updated: ${newGreeting}`)
})untrack runs a callback in an inert context, so reactive reads inside the callback
do not create implicit subscriptions or side effects. It also returns the callback result.
import { effect, resource, signal, untrack } from "kiru"
const id = signal(1)
const loadUser = async () => fetch(`/users/${id.value}`)
const user = resource(loadUser)
effect(() => {
const userPromise = untrack(() => loadUser())
// Reading id.value here would normally subscribe this effect to "id".
// untrack prevents that implicit subscription.
})Use untrack when you intentionally need a one-off read in an effect or computed
without wiring that read into reactivity.
General usage
In Kiru 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, Kiru 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 effect can all be created locally in Kiru
components by creating them within the setup scope.
This will allow them to be persisted across renders and automatically
disposed of when the component unmounts.
import { signal, computed, effect } from "kiru"
function App() {
// Create a local signal
const userName = signal("bob")
const userGreeting = computed(() => `Hello, ${userName}!`)
effect([userName], console.log)
// return a render function
return () => (
<div>
<h1>{userGreeting}</h1>
<input
type="text"
value={userName}
oninput={(e) => (userName.value = e.target.value)}
/>
</div>
)
} Signals in style objects
Signals can also be used inside style objects. Kiru will automatically keep
track of the signal's value and update the property whenever it changes.
import { signal } from "kiru"
function App() {
const bgColor = signal("rebeccapurple")
const toggleColor = () => {
bgColor.value = bgColor.value === "rebeccapurple" ? "tomato" : "rebeccapurple"
}
return () => (
<button
style={{
backgroundColor: bgColor,
color: "white",
}}
onclick={toggleColor}
>
Toggle color
</button>
)
}Signals as ref
Signals can also be passed directly to the ref attribute. This gives you a reactive
way to observe element attachment/detachment without creating a separate ref object.
import { signal } from "kiru"
function App() {
const buttonEl = signal<HTMLButtonElement | null>(null)
buttonEl.subscribe((el) => {
if (el) console.log("button mounted")
})
return () => <button ref={buttonEl}>Click me</button>
}For RefObject and callback patterns, see the ref docs.
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 = signal("bob")
effect(() => console.log(userName.value))
return () => (
<div>
<input type="text" bind:value={userName} />
</div>
)
} Use resource to manage asynchronous data fetching with automatic caching,
refetching, and loading states. A Resource is a special signal that holds
the result of an async operation and tracks loading and error states.
Basic Usage
You can create a resource in three ways:
1. With a single source signal:
import { resource } from "kiru"
const userId = signal(1)
const data = resource(userId, async (id, { signal }) => {
const response = await fetch(`/api/data?id=${id}`, { signal })
return response.json()
})2. With multiple source signals (as an object):
const search = signal("")
const limit = signal(10)
const data = resource({ search, limit }, async (src, { signal }) => {
const response = await fetch(
`/api/data?q=${src.search}&limit=${src.limit}`,
{ signal }
)
return response.json()
})3. Without a source:
const search = signal("")
const limit = signal(10)
const data = resource(async ({ signal }) => {
// Signals observed synchronously are automatically tracked
const response = await fetch(
`/api/data?q=${search}&limit=${limit}`,
{ signal }
)
return response.json()
})Auto-tracking
Resources automatically track any signals that are observed synchronously during the fetcher function's execution. This applies whether or not you provide a source parameter.
Only signals observed before the first await are tracked. Signals
accessed after an await won't trigger refetches. If you need to observe
signals later in the async flow, either provide them as a source or read
their values at the start of the function.
A Resource provides:
- value: The resolved data (or
undefinedwhile loading) - isPending: A signal indicating if the resource is currently loading
- error: A signal containing any error that occurred during fetching
- promise: The underlying promise for the current fetch
- refetch(): Manually trigger a refetch of the data
- dispose(): Manually dispose of the resource
import { signal, computed, resource, Derive } from "kiru"
interface User {
id: number
firstName: string
lastName: string
}
const search = signal("")
const limit = signal(10)
// With a single source signal
// Auto-tracking is still active - any other signals observed synchronously
// will also trigger refetches
const users = resource(search, async (search, { signal }) => {
const res = await fetch(`https://dummyjson.com/users/search?q=${search}`, {
signal,
})
return res.json() as Promise<{ users: User[] }>
})
// With multiple source signals
const usersWithLimit = resource({ search, limit }, async (src, { signal }) => {
const res = await fetch(
`https://dummyjson.com/users/search?q=${src.search}&limit=${src.limit}`,
{ signal }
)
return res.json() as Promise<{ users: User[] }>
})
// Without a source - relies entirely on auto-tracking
const usersAutoTracked = resource(async ({ signal }) => {
// Signals observed synchronously (before any await) are automatically tracked
const res = await fetch(
`https://dummyjson.com/users/search?q=${search}&limit=${limit}`,
{ signal }
)
return res.json() as Promise<{ users: User[] }>
})
function App() {
return (
<>
<input placeholder="search" bind:value={search} />
<input type="number" bind:value={limit} />
<button onclick={() => usersWithLimit.refetch()}>Refetch</button>
<Derive from={usersWithLimit} fallback={<div>Loading...</div>}>
{(data, isStale) => (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<ul>
{data.users.map((user) => (
<li key={user.id}>
{user.firstName} {user.lastName}
</li>
))}
</ul>
</div>
)}
</Derive>
</>
)
}Resources can be created globally or locally within components. When created locally, they are automatically disposed when the component unmounts.
Using with Derive
The <Derive /> component is perfect for rendering resource data with fallback & pending states:
<Derive from={data} fallback={<div>Loading...</div>}>
{(result, isStale) => (
<div className={isStale ? "opacity-50" : ""}>
{/* render result */}
</div>
)}
</Derive>The second parameter isStale indicates whether the resource is currently refetching
while displaying cached data.
Handling Errors
Access the error state through the error signal:
<Derive from={data.error}>
{(err) => err && <div className="error">{err.message}</div>}
</Derive>Or use an <ErrorBoundary>:
<ErrorBoundary>
<Derive from={data}>
{/* render result */}
</Derive>
</ErrorBoundary>