Skip to content
kerfjsGitHub

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>  )}
  1. State is a box. Read and write .value, including in JSX (W015).
  2. Compute from other values with derived, not watch or the component body (W001).
  3. JSX is live. Each {expr} and attribute re-evaluates when its inputs change.
  4. Props are current. Destructure one level (W006). Read in derived, watch, JSX or handlers. const x = props.y is a live read-only alias; let x = props.y and props.y = write locally; only bind:y={box} writes out.
  5. The body runs once. No state-based early returns (W040) or let counters (W041). Branch in JSX.
  6. Pass a box only to give write access (value={coupon}, child prop typed State<string | null>), or so list rows can read one entry: cart.get(p.id) re-runs one row, cart[p.id] or cart.value[p.id] re-runs them all (W017), in the list and in the row component alike.
  7. Lists and conditionals: .map with key (W010), &&, ternaries. Keep items stable: replace only the changed item (p.id === id ? { ...p, price } : p), never ({ ...p }) every item (W014).
  8. Copy a prop into state with state.linked(() => prop), not state(prop) (W005). untrack(() => …) is a snapshot that never updates: never render it (W018).
  9. Side effects go in watch(() => source, value => cleanup?); DOM work in onMount. flush() applies pending DOM updates now.