Core
The rules
Nine rules cover how every kerf component behaves. The example below uses most of them. Nearly all carry a code, so when you break one, the build names it and says how to fix it.
A quantity, plus shipping and a total that are worked out from it. Shipping turns free at three items.
Your basket
1 × $12 · shipping $4
Total $16
import { derived, state } from '@kerf-js/core'import { Button, Card } from '@kerf-js/veneer'export default function Basket() { const price = 12 const qty = state(1) const shipping = derived(() => (qty.value >= 3 ? 0 : 4)) const total = derived( () => price * qty.value + shipping.value, ) return ( <Card title="Your basket"> <p> {qty.value} × $12 · shipping ${shipping.value} </p> <p> <strong>Total ${total.value}</strong> </p> <Button onClick={() => qty.value++}>Add one</Button> <Button emphasis="ghost" tone="neutral" onClick={() => (qty.value = 1)} > Reset </Button> </Card> )}- Try this
- Press Add one twice. At three items shipping drops to $0 and the total changes with it.
- Why it matters
- You say what the total is once, and it can never go stale. Most bugs in forms and carts come from keeping such values in sync by hand.
tsx
import { derived } from '@kerf-js/core'export function Cart({ items, taxRate }: { items: { id: string; name: string; price: number }[] taxRate: number}) { const subtotal = derived(() => items.reduce((s, i) => s + i.price, 0), ) const total = derived(() => subtotal.value * (1 + taxRate)) return ( <section> <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> <p>{total.value.toFixed(2)}</p> </section> )}- State is a box. Read and write
.value, including in JSX (W015). - Compute from other values with
derived, notwatchor the component body (W001). - JSX is live. Each
{expr}and attribute re-evaluates when its inputs change. - Props are current. Destructure one level (W006). Read in
derived,watch, JSX or handlers.const x = props.yis a live read-only alias;let x = props.yandprops.y =write locally; onlybind:y={box}writes out. - The body runs once. No state-based early returns (W040) or
letcounters (W041). Branch in JSX. - Pass a box only to give write access (
value={coupon}, child prop typedState<string | null>), or so list rows can read one entry:cart.get(p.id)re-runs one row,cart[p.id]orcart.value[p.id]re-runs them all (W017), in the list and in the row component alike. - Lists and conditionals:
.mapwithkey(W010),&&, ternaries. Keep items stable: replace only the changed item (p.id === id ? { ...p, price } : p), never({ ...p })every item (W014). - Copy a prop into state with
state.linked(() => prop), notstate(prop)(W005).untrack(() => …)is a snapshot that never updates: never render it (W018). - Side effects go in
watch(() => source, value => cleanup?); DOM work inonMount.flush()applies pending DOM updates now.