Component
In the AIR Stack, components carry HTML's mental model. A native <input> element is not a dumb box that waits for its parent to set a value — it updates itself when the user types, has its own default type, manages its own selection, and emits events when things change. The parent can configure it with attributes, but the element governs itself.
AIR Stack components work the exact same way. Each component is a self-governing unit with its own responsibility. A PriceTag formats and displays a price. A StatusBadge resolves a status into a visual indicator. A QuantitySelector manages a number within boundaries. Each one handles its concern without waiting for the parent to tell it how.
What Makes a Component?
A piece of UI becomes a component when it has its own:
- State — it holds data that belongs to itself (loading, selected, count), used only inside the component scope
- Behavior — it does something (format, validate, toggle)
- Side-effect — it triggers something (fetch, sync, animate)
For example: A PriceTag has state (the derived formatted value) and behavior (formatting, error handling):
import { setup, render, derived } from '@anchorlib/react';
const PriceTag = setup<{ amount: number; currency?: string }>((props) => {
const formatter = derived(() => new Intl.NumberFormat('en-US', {
style: 'currency',
currency: props.currency ?? 'USD',
}));
const display = derived(() => {
try { return formatter.value.format(props.amount); } catch { return 'N/A'; }
});
return render(() => <span className="price">{ display.value }</span>);
});import { setup, derived } from '@anchorlib/solid';
const PriceTag = setup<{ amount: number; currency?: string }>((props) => {
const formatter = derived(() => new Intl.NumberFormat('en-US', {
style: 'currency',
currency: props.currency ?? 'USD',
}));
const display = derived(() => {
try { return formatter.value.format(props.amount); } catch { return 'N/A'; }
});
return <span class="price">{ display.value }</span>;
});// In React, use pass-by-reference — lazy read for reactive updates
<PriceTag amount={$use(item, 'price')} />
<PriceTag amount={$use(cart, 'subtotal')} />
<PriceTag amount={$use(cart, 'total')} />// In SolidJS, expressions automatically wrapped as a lazy read
<PriceTag amount={item.price} />
<PriceTag amount={cart.subtotal} />
<PriceTag amount={cart.total} />What you learned
Creating a stable component that only runs-once. It survives parent re-renders, have stable closure, and its internal state and views reacts individually when the data it reads changes.
From these two examples, you know:
derived()creates a cached value that automatically re-create only when any dependency changesformatterrecomputes only whencurrencychanges, avoiding unnecessaryIntl.NumberFormatconstructiondisplayrecomputes whenamountorformatterchanges, chaining dependencies automatically- React:
setup()makes the component function run once, guaranteeing stable closure to survive parent re-renders. - React:
render()creates a reactive view that belongs to the component itself. - SolidJS: the function already runs once, and JSX expressions are tracked automatically
Presentational Components
A piece of UI becomes a presentational component when it transforms data into a visual output — formatting, mapping, or resolving values into display. The component centralizes the transformation so every consumer gets the same result.
From Props
Data comes from the parent as props. The component transforms it.
For example: A StatusBadge resolves a status code into a visual indicator, centralizing the mapping in one place:
import { setup, render, derived } from '@anchorlib/react';
const StatusBadge = setup<{ status: 'online' | 'away' | 'offline' }>((props) => {
const label = derived(() => {
switch (props.status) {
case 'online':
return '🟢 Online';
case 'away':
return '🟡 Away';
case 'offline':
return '⚫ Offline';
default:
return '⚪ Unknown';
}
});
return render(() => (
<span className={ `badge ${ props.status }` }>{ label.value }</span>
));
});import { setup, derived } from '@anchorlib/solid';
const StatusBadge = setup<{ status: 'online' | 'away' | 'offline' }>((props) => {
const label = derived(() => {
switch (props.status) {
case 'online':
return '🟢 Online';
case 'away':
return '🟡 Away';
case 'offline':
return '⚫ Offline';
default:
return '⚪ Unknown';
}
});
return <span class={ `badge ${ props.status }` }>{ label.value }</span>;
});// In React, use pass-by-reference — lazy read for reactive updates
<StatusBadge status={ $use(user, 'status') } />// In SolidJS, expressions automatically wrapped as a lazy read
<StatusBadge status={ user.status } />What you learned
Using derived() to centralize a value mapping so the view doesn't change when new statuses are added.
From these two examples, you know:
derived()centralizes the status-to-label mapping so the view stays the same when new statuses are addedlabel.valuereads the cached result, only recomputing whenstatuschangesStatusBadgeis one-way: it receivesstatusfrom the parent and presents it
From Global State
Data comes from a shared store, not from props. The component reads it directly.
For example: A ThemeIndicator reads the current theme from global state:
import { setup, render } from '@anchorlib/react';
import { appState } from '../states/app.js';
const ThemeIndicator = setup(() => {
return render(() => (
<span className={ `theme ${ appState.theme }` }>{ appState.theme }</span>
));
});import { setup } from '@anchorlib/solid';
import { appState } from '../states/app.js';
const ThemeIndicator = setup(() => {
return <span class={ `theme ${ appState.theme }` }>{ appState.theme }</span>;
});<ThemeIndicator /><ThemeIndicator />What you learned
Reading global reactive state directly inside a component, without props.
From these two examples, you know:
appStateis amutable()store — reactive across components- React:
setup()ensures reactive store reads instead of capturing stale values - SolidJS: store access is tracked automatically
- No props needed — the component reads from the store directly
Interactive Components
A piece of UI becomes an interactive component when it responds to user input. Interactive components can be two-way — they receive data AND write back to it. This is where Bindable comes in.
Instant Interaction
The simplest form: one action, one state change.
For example: A Toggle receives a boolean value and flips it on click:
import { setup, render, type Bindable } from '@anchorlib/react';
const Toggle = setup<{ value?: Bindable<boolean>; onChange?: () => void }>((props) => {
const toggle = () => {
props.value = !props.value;
props.onChange?.();
};
return render(() => (
<button className={ props.value ? 'on' : 'off' } onClick={ toggle }>
{ props.value ? 'On' : 'Off' }
</button>
));
});import { setup, type Bindable } from '@anchorlib/solid';
const Toggle = setup<{ value?: Bindable<boolean>; onChange?: () => void }>((props) => {
const toggle = () => {
props.value = !props.value;
props.onChange?.();
};
return (
<button classList={ { on: props.value, off: !props.value } } onClick={ toggle }>
{ props.value ? 'On' : 'Off' }
</button>
);
});// $bind() connects the prop to the parent's state — writes propagate automatically
<Toggle value={ $bind(formState, 'notifications') } onChange={ () => savePrefs() } />// $bind() connects the prop to the parent's state — writes propagate automatically
<Toggle value={ $bind(formState, 'notifications') } onChange={ () => savePrefs() } />What you learned
Creating a two-way component where the component writes directly to the parent's state.
From these two examples, you know:
Bindable<boolean>makesvaluetwo-way: the component reads and writes it directly viaprops.value = ...$bind()connects the prop to the parent's state, so writes propagate to the parent automaticallyonChangeis for side effects (logging, analytics) — not state control, the component already wrote the value- SolidJS:
bindable()wraps the component to enable two-way prop assignment - React:
Bindableprops are writable insidesetup()without additional wrappers
Self-Binding
Some components don't receive a trigger element — they find it themselves.
For example: A Tooltip shows text when the user hovers over its parent element. It sits alongside siblings instead of wrapping them, so it never re-renders sibling content:
import { setup, render, mutable, onMount, onCleanup } from '@anchorlib/react';
const Tooltip = setup<{ text: string }>((props) => {
const state = mutable({ visible: false });
let ref: HTMLSpanElement;
onMount(() => {
const parent = ref.parentElement;
if (!parent) return;
const show = () => { state.visible = true; };
const hide = () => { state.visible = false; };
parent.addEventListener('mouseenter', show);
parent.addEventListener('mouseleave', hide);
onCleanup(() => {
parent.removeEventListener('mouseenter', show);
parent.removeEventListener('mouseleave', hide);
});
});
return render(() => (
<span ref={ (el) => { ref = el; } } className="tooltip-anchor">
{ state.visible && <span className="tooltip">{ props.text }</span> }
</span>
));
});import { setup, mutable, onMount, onCleanup } from '@anchorlib/solid';
const Tooltip = setup<{ text: string }>((props) => {
const state = mutable({ visible: false });
let ref: HTMLSpanElement;
onMount(() => {
const parent = ref.parentElement;
if (!parent) return;
const show = () => { state.visible = true; };
const hide = () => { state.visible = false; };
parent.addEventListener('mouseenter', show);
parent.addEventListener('mouseleave', hide);
onCleanup(() => {
parent.removeEventListener('mouseenter', show);
parent.removeEventListener('mouseleave', hide);
});
});
return (
<span ref={ (el) => { ref = el; } } class="tooltip-anchor">
{ state.visible && <span class="tooltip">{ props.text }</span> }
</span>
);
});<div className="user-card">
<Avatar src={ user.photo } />
<Tooltip text="Click to edit profile" />
</div><div class="user-card">
<Avatar src={ user.photo } />
<Tooltip text="Click to edit profile" />
</div>What you learned
Creating a standalone component that binds to its parent element instead of wrapping children.
From these two examples, you know:
- Tooltip sits alongside siblings instead of wrapping them, so showing/hiding never re-renders sibling content
mutable()creates reactive state; only the tooltip's view updates whenvisiblechangesonMount()runs after the component is in the DOM, makingref.parentElementavailable for event bindingonCleanup()removes listeners when the component is destroyed, preventing memory leaks
Interaction Sequence
A drag is a sequence — start, continue, end. The component owns the full lifecycle.
For example: A Slider tracks the pointer across the full drag sequence:
import { setup, render, mutable, onCleanup, type Bindable } from '@anchorlib/react';
const Slider = setup<{ value?: Bindable<number>; min?: number; max?: number }>((props) => {
const min = props.min ?? 0;
const max = props.max ?? 100;
const state = mutable({ dragging: false });
let trackRef: HTMLDivElement;
const updateValue = (clientX: number) => {
const rect = trackRef.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
props.value = Math.round(min + ratio * (max - min));
};
const cleanup = () => {
state.dragging = false;
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
};
const onPointerMove = (e: PointerEvent) => updateValue(e.clientX);
const onPointerUp = () => cleanup();
const onPointerDown = (e: PointerEvent) => {
state.dragging = true;
updateValue(e.clientX);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
};
onCleanup(cleanup);
return render(() => {
const percent = ((props.value - min) / (max - min)) * 100;
return (
<div
className={ `slider ${ state.dragging ? 'dragging' : '' }` }
ref={ (el) => { trackRef = el; } }
onPointerDown={ onPointerDown }
>
<div className="slider-fill" style={ { width: `${ percent }%` } } />
<div className="slider-thumb" style={ { left: `${ percent }%` } } />
</div>
);
});
});import { setup, mutable, onCleanup, type Bindable } from '@anchorlib/solid';
const Slider = setup<{ value?: Bindable<number>; min?: number; max?: number }>((props) => {
const min = props.min ?? 0;
const max = props.max ?? 100;
const state = mutable({ dragging: false });
let trackRef: HTMLDivElement;
const updateValue = (clientX: number) => {
const rect = trackRef.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
props.value = Math.round(min + ratio * (max - min));
};
const cleanup = () => {
state.dragging = false;
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
};
const onPointerMove = (e: PointerEvent) => updateValue(e.clientX);
const onPointerUp = () => cleanup();
const onPointerDown = (e: PointerEvent) => {
state.dragging = true;
updateValue(e.clientX);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
};
onCleanup(cleanup);
return (
<div
class={ `slider ${ state.dragging ? 'dragging' : '' }` }
ref={ (el) => { trackRef = el; } }
onPointerDown={ onPointerDown }
>
<div class="slider-fill" style={ { width: `${ ((props.value - min) / (max - min)) * 100 }%` } } />
<div class="slider-thumb" style={ { left: `${ ((props.value - min) / (max - min)) * 100 }%` } } />
</div>
);
});<Slider value={ $bind(settings, 'volume') } min={ 0 } max={ 100 } /><Slider value={ $bind(settings, 'volume') } min={ 0 } max={ 100 } />What you learned
Managing a drag interaction sequence where the component owns the full pointer lifecycle.
From these two examples, you know:
Bindablemakesvaluetwo-way, so pointer movement writes directly to the parent's state via$bind()cleanup()is shared betweenonPointerUpandonCleanup, covering both normal drag end and mid-drag removalmutable({ dragging })tracks internal state the parent doesn't need- Document-level listeners capture pointer events outside the slider element during drag
Owning State
A piece of UI becomes a stateful component when it tracks values that serve its concern. The state can be internal (only the component needs it), bound from the parent (Bindable), or both.
Guarding Boundaries
The component enforces rules on a two-way value before writing it.
For example: A QuantitySelector manages a number within boundaries. It has both one-way props (min, max) and a two-way prop (value):
import { setup, render, type Bindable } from '@anchorlib/react';
const QuantitySelector = setup<{
value?: Bindable<number>;
min?: number;
max?: number;
onChange?: () => void;
}>((props) => {
const min = props.min ?? 0;
const max = props.max ?? Infinity;
const increment = () => {
if (props.value < max) {
props.value++;
props.onChange?.();
}
};
const decrement = () => {
if (props.value > min) {
props.value--;
props.onChange?.();
}
};
return render(() => (
<div className="quantity">
<button disabled={ props.value <= min } onClick={ decrement }>−</button>
<span>{ props.value }</span>
<button disabled={ props.value >= max } onClick={ increment }>+</button>
</div>
));
});import { setup, type Bindable } from '@anchorlib/solid';
const QuantitySelector = setup<{
value?: Bindable<number>;
min?: number;
max?: number;
onChange?: () => void;
}>((props) => {
const min = props.min ?? 0;
const max = props.max ?? Infinity;
const increment = () => {
if (props.value < max) {
props.value++;
props.onChange?.();
}
};
const decrement = () => {
if (props.value > min) {
props.value--;
props.onChange?.();
}
};
return (
<div class="quantity">
<button disabled={ props.value <= min } onClick={ decrement }>−</button>
<span>{ props.value }</span>
<button disabled={ props.value >= max } onClick={ increment }>+</button>
</div>
);
});<QuantitySelector value={ $bind(cartItem, 'quantity') } min={ 1 } max={ 99 } /><QuantitySelector value={ $bind(cartItem, 'quantity') } min={ 1 } max={ 99 } />What you learned
Creating a component that owns and guards a two-way value with boundary enforcement.
From these two examples, you know:
valueisBindable<number>(two-way): the component enforces boundaries before writingminandmaxare one-way: the component reads them but never changes them- The component guarantees the value stays in range, so the parent never needs to guard against invalid values
Dynamic Structure
The component builds its own structure from configuration, then manages interaction through it.
For example: A RatingStars builds its structure from max, then manages the rating through click:
import { setup, render, type Bindable } from '@anchorlib/react';
const RatingStars = setup<{
value?: Bindable<number>;
max?: number;
onChange?: () => void;
}>((props) => {
const max = props.max ?? 5;
const rate = (star: number) => {
props.value = star;
props.onChange?.();
};
return render(() => (
<div className="rating">
{ Array.from({ length: max }, (_, i) => (
<button
className={ i < props.value ? 'filled' : 'empty' }
onClick={ () => rate(i + 1) }
>
★
</button>
)) }
</div>
));
});import { setup, type Bindable } from '@anchorlib/solid';
import { Index } from 'solid-js';
const RatingStars = setup<{
value?: Bindable<number>;
max?: number;
onChange?: () => void;
}>((props) => {
const max = props.max ?? 5;
const rate = (star: number) => {
props.value = star;
props.onChange?.();
};
return (
<div class="rating">
<Index each={ Array.from({ length: max }) }>
{ (_, i) => (
<button
classList={ { filled: i < props.value, empty: i >= props.value } }
onClick={ () => rate(i + 1) }
>
★
</button>
) }
</Index>
</div>
);
});<RatingStars value={ $bind(review, 'rating') } max={ 5 } /><RatingStars value={ $bind(review, 'rating') } max={ 5 } />What you learned
Building a dynamic structure from configuration, then managing two-way interaction through it.
From these two examples, you know:
max(one-way) determines how many stars to render at creationvalue(two-way) is written when the user clicks a star- SolidJS:
Indexrenders a fixed list without re-creating DOM nodes; onlyclassListbindings update reactively
Calling Remote Functions
IRPC functions are declared on the server and called on the client like local functions. How you call them determines when they execute and whether they react to changes.
Direct Call
A component calls a remote function in response to a user action — click, submit, gesture. The call happens once, when the user triggers it.
For example: A LogoutButton calls signOut() when the user clicks:
import { setup, render } from '@anchorlib/react';
import { signOut } from '../rpc/auth/index.js';
const LogoutButton = setup(() => {
const handleClick = () => {
signOut().then(() => {
window.location.href = '/login';
});
};
return render(() => (
<button className="btn-logout" onClick={ handleClick }>Sign Out</button>
));
});import { setup } from '@anchorlib/solid';
import { signOut } from '../rpc/auth/index.js';
const LogoutButton = setup(() => {
const handleClick = () => {
signOut().then(() => {
window.location.href = '/login';
});
};
return <button class="btn-logout" onClick={ handleClick }>Sign Out</button>;
});<LogoutButton /><LogoutButton />What you learned
Using a direct call for a user-triggered action the view doesn't need to track.
Once
The component needs to call a function once in the browser with static arguments. The result won't update if props change later.
For example: A CurrentPrice shows the price for a fixed currency — fetched once, never re-fetched:
import { setup, render } from '@anchorlib/react';
import { watchPrice } from '../rpc/market/index.js';
const CurrentPrice = setup(() => {
const stock = watchPrice.once('USD');
return render(() => (
<span className="price">
${ stock.data.price.toFixed(2) }
{ stock.status === 'pending' ? ' ⏳' : '' }
</span>
));
});import { setup } from '@anchorlib/solid';
import { watchPrice } from '../rpc/market/index.js';
const CurrentPrice = setup(() => {
const stock = watchPrice.once('USD');
return (
<span class="price">
${ stock.data.price.toFixed(2) }
{ stock.status === 'pending' ? ' ⏳' : '' }
</span>
);
});<CurrentPrice /><CurrentPrice />What you learned
Calling a remote function once in the browser with static arguments.
From these two examples, you know:
watchPrice.once('USD')calls the function once on mount with a fixed argument — skipped during SSR- The result won't re-fetch if the component re-renders or props change
stream.statusandstream.dataare still reactive — the view updates when the response arrives
With
The component needs data immediately in the browser and the data must stay current when arguments change.
For example: A LivePrice shows a live price stream that reconnects when the symbol changes:
import { setup, render } from '@anchorlib/react';
import { watchPrice } from '../rpc/market/index.js';
const LivePrice = setup<{ symbol: string }>((props) => {
const stream = watchPrice.with(() => [props.symbol]);
return render(() => (
<span className="price live">
${ stream.data.price.toFixed(2) }
{ stream.status === 'pending' ? ' 🟢' : ' 🛑' }
</span>
));
});import { setup } from '@anchorlib/solid';
import { watchPrice } from '../rpc/market/index.js';
const LivePrice = setup<{ symbol: string }>((props) => {
const stream = watchPrice.with(() => [props.symbol]);
return (
<span class="price live">
${ stream.data.price.toFixed(2) }
{ stream.status === 'pending' ? ' 🟢' : ' 🛑' }
</span>
);
});<LivePrice symbol={ $use(portfolio, 'selectedSymbol') } /><LivePrice symbol={ portfolio.selectedSymbol } />What you learned
Connecting to a live data stream that starts immediately on browser and reconnects when arguments change.
From these two examples, you know:
watchPrice.with(() => [props.symbol])starts the stream immediately on browser — skipped during SSR- When
props.symbolchanges, the stream reconnects with the new arguments automatically - No
onMount()needed —.with()handles browser-only execution stream.dataandstream.statusare reactive — the view updates as data arrives
When
The component should not call the function on mount — only when the user actually provides input.
For example: A SearchBox searches products as the user types — not on page load:
import { setup, render, mutable } from '@anchorlib/react';
import { searchProducts } from '../rpc/product/index.js';
const SearchBox = setup(() => {
const state = mutable({ query: '' });
const results = searchProducts.when(() => [state.query.trim()], 300);
return render(() => (
<div className="search">
<input
value={ state.query }
onInput={ (e) => { state.query = e.target.value; } }
placeholder="Search..."
/>
{ results.status === 'pending' && <span className="spinner" /> }
{ results.status === 'success' && (
<ul>
{ results.data.map((item) => (
<li>{ item.name }</li>
)) }
</ul>
) }
</div>
));
});import { setup, mutable } from '@anchorlib/solid';
import { searchProducts } from '../rpc/product/index.js';
const SearchBox = setup(() => {
const state = mutable({ query: '' });
const results = searchProducts.when(() => [state.query.trim()], 300);
return (
<div class="search">
<input
value={ state.query }
onInput={ (e) => { state.query = e.currentTarget.value; } }
placeholder="Search..."
/>
{ results.status === 'pending' && <span class="spinner" /> }
{ results.status === 'success' && (
<ul>
{ results.data.map((item) => (
<li>{ item.name }</li>
)) }
</ul>
) }
</div>
);
});<SearchBox /><SearchBox />What you learned
Using .when() to trigger an IRPC function only when arguments change, with debounce.
From these two examples, you know:
searchProducts.when(() => [query], 300)— only fires whenstate.queryactually changes, with 300ms debounce.when()skips the initial mount — no empty search on page load- Default debounce is 0 (microtask) — if 100 synchronous changes happen, only the last runs
results.status(idle | pending | success | error) andresults.dataare reactive — no manual state needed
Function Signatures
| Call | When it runs | Reactive | Use case |
|---|---|---|---|
fn(arg) | Immediately when called | No | Event handlers, user-triggered actions |
fn.once(arg) | Immediately (browser only) | No | One-time browser actions |
fn.with(() => [args]) | Immediately (browser), re-runs when arguments change | Yes | Live streams, auto-refreshing data |
fn.when(() => [args], ms) | Only when arguments change, debounced | Yes | Search, filtering, user-driven queries |
Managing Lifecycle
A piece of UI becomes a lifecycle component when it works with external resources that need to start and stop — timers, event listeners, manual subscriptions. Code in the component body runs during both SSR and browser — use onMount() for browser-only work.
IRPC streams and reactive calls manage their own lifecycle automatically through
.with()and.when(). UseonMount()for non-IRPC resources like timers and DOM event listeners.
For example: A Countdown manages a timer that starts in the browser and cleans up when the component is removed:
import { setup, render, mutable, onMount, onCleanup } from '@anchorlib/react';
const Countdown = setup<{ seconds: number; onComplete?: () => void }>((props) => {
const state = mutable({ remaining: props.seconds });
onMount(() => {
const interval = setInterval(() => {
if (state.remaining > 0) {
state.remaining--;
} else {
clearInterval(interval);
props.onComplete?.();
}
}, 1000);
onCleanup(() => clearInterval(interval));
});
return render(() => (
<span className="countdown">{ state.remaining }s</span>
));
});import { setup, mutable, onMount, onCleanup } from '@anchorlib/solid';
const Countdown = setup<{ seconds: number; onComplete?: () => void }>((props) => {
const state = mutable({ remaining: props.seconds });
onMount(() => {
const interval = setInterval(() => {
if (state.remaining > 0) {
state.remaining--;
} else {
clearInterval(interval);
props.onComplete?.();
}
}, 1000);
onCleanup(() => clearInterval(interval));
});
return <span class="countdown">{ state.remaining }s</span>;
});<Countdown seconds={ 30 } onComplete={ () => expireOffer() } /><Countdown seconds={ 30 } onComplete={ () => expireOffer() } />What you learned
Managing a browser-only timer that is safe during SSR and cleans up when the component is removed.
From these two examples, you know:
mutable()creates reactive state safely anywhere, including during SSRsetIntervalruns insideonMount()so the timer only starts in the browseronCleanup()insideonMount()ties the interval cleanup to the component's lifecycle
Reacting to Change
A piece of UI becomes reactive when some of its work re-runs in response to changing inputs.
derived()— produces a cached value that recomputes when its dependencies changeeffect()— performs a side effect when its dependencies change
For example: A ProductCard uses derived() to compute a line total from price × quantity:
const total = derived(() => (props.product.price * props.quantity).toFixed(2));The total recomputes automatically when either price or quantity changes. No manual recalculation, no stale values.
For example: An effect() saves the theme to localStorage when it changes:
effect(() => {
localStorage.setItem('theme', appState.theme);
});The effect re-runs automatically when appState.theme changes. No manual event listener, no subscription management.
IRPC reactive calls (
.with(),.when()) are covered in Calling Remote Functions.derived()andeffect()are for local reactive computations and side effects.
Domain Boundaries
A component crosses its boundary when it handles work that doesn't serve its concern.
For example: A ProductCard that displays a product, manages quantity, and computes a line total — that's one concern:
import { setup, render, derived, type Bindable } from '@anchorlib/react';
const ProductCard = setup<{ product: Product; quantity?: Bindable<number> }>((props) => {
const total = derived(() => (props.product.price * props.quantity).toFixed(2));
const increment = () => { props.quantity++; };
const decrement = () => { if (props.quantity > 1) props.quantity--; };
return render(() => (
<div className="product-card">
<span>{ props.product.name }</span>
<span>${ props.product.price }</span>
<div className="quantity">
<button disabled={ props.quantity <= 1 } onClick={ decrement }>−</button>
<span>{ props.quantity }</span>
<button onClick={ increment }>+</button>
</div>
<span className="total">${ total.value }</span>
</div>
));
});import { setup, derived, type Bindable } from '@anchorlib/solid';
const ProductCard = setup<{ product: Product; quantity?: Bindable<number> }>((props) => {
const total = derived(() => (props.product.price * props.quantity).toFixed(2));
const increment = () => { props.quantity++; };
const decrement = () => { if (props.quantity > 1) props.quantity--; };
return (
<div class="product-card">
<span>{ props.product.name }</span>
<span>${ props.product.price }</span>
<div class="quantity">
<button disabled={ props.quantity <= 1 } onClick={ decrement }>−</button>
<span>{ props.quantity }</span>
<button onClick={ increment }>+</button>
</div>
<span class="total">${ total.value }</span>
</div>
);
});<ProductCard product={ item } quantity={ $bind(cartItem, 'quantity') } /><ProductCard product={ item } quantity={ $bind(cartItem, 'quantity') } />What you learned
Combining one-way display and two-way interaction within a single concern.
From these two examples, you know:
productis one-way: the card displays it but doesn't modify itquantityisBindable(two-way): the card adjusts it, and the parent's state updates automaticallyderived()computes the total reactively fromprice × quantity, so the view always reflects the current math
The boundary is crossed when work leaves the component's domain. A ProductCard that also refreshes the user's profile, updates the shipping estimate, or syncs the cart to the server — that's crossing into concerns that belong elsewhere.
The question isn't "how many things does this component do?" It's "does everything this component does serve the same concern?"
Learn More
- Styling — When a concern needs its own state, behavior, and reactivity
- Static UI — When UI should remain inline, and when it should graduate
- Reactive UI — Presenting reactive data without owning it
- Form Components — User-driven form components with built-in validation