Skip to content
weftGitHub

Core

Props

Tip owns the bill. Price is a separate component that only displays the number it is given.

Split the bill

Each of you pays $20.00

import { state } from '@weft-js/core'import { Button, Card } from '@weft-js/warp'function Price(props: { dollars: number }) {  return <strong>${props.dollars.toFixed(2)}</strong>}export default function Tip() {  const bill = state(40)  return (    <Card title="Split the bill">      <p>Each of you pays <Price dollars={bill.value / 2} /></p>      <Button onClick={() => (bill.value += 10)}>Add a $10 dish</Button>    </Card>  )}
Try this
Press Add a $10 dish. Price updates, though neither component ran again.
Why it matters
Passing a value to a child looks the same as in React, and the child stays up to date without re-rendering.

Types

Leave a component's props unannotated. weft infers them from its callers and its body, in the editor (the plugin in tsconfig.json), weft check, weft tsc and weft dev. Nothing is left at run time.

tsx
function Row(props) {  return <li><button type="button" onClick={props.onPick}>{props.label.toUpperCase()}</button></li>}export default function List() {  return <ul><Row label="One" onPick={() => console.log('picked one')} /></ul>}
CaseWhat you get
Several callersA union if the body handles it; if not, W084 at the odd caller
Some callers omit a propOptional; a default value also types it
<Button {...props} />Button's props
A component in a variable, map or propIts sites count, and prop sets merge
Callers that differ by a string propA union of shapes the body can narrow
src/routes default exportRouteProps
An exportweft dev or weft fix writes the type with an /*inferred*/ comment, so it shows in a diff. W082 when callers move on; delete the comment to keep the type
An any from a callerThe body's use, or any, with W083 naming where it came from
No callerW080, with what to do
bind:x={box}The box's value, and it satisfies a required x under weft tsc and in the editor

Write a type yourself when two props must agree (a type parameter), when the file is .jsx, or for a public API with no caller in this repository. Props<typeof Button> names another component's props. tsc alone reports TS7006 on an inferred parameter; run weft tsc in CI. check: { props: false } turns this off; 'manual' leaves writing to weft fix.

Reactivity

The compiler turns a component's props into live reads, so a parent writing count={count.value} is correct.

FormBehaviour
({ a, b }) =>Each name is a live read of props.a
(props) =>props.a is a live getter
({ size = 'md' }) =>Default applied on every read when the prop is undefined
({ a, ...rest }) =>rest is a live object; <div {...rest}> stays live
const x = props.y in the bodyx is another name for the prop: each use reads it live, at no cost
const { a: b, c = 1, ...rest } = props in the bodyThe same as in the parameter: renames, defaults and rest stay live
const n = props.items.length in the bodyA path off a prop is live too
({ user: { name } }) =>W006: nested destructuring can't stay live. Read user.name at the use site (autofix)
  • The local isn't a copy: every use, closures included, reads the prop as it is at that moment. For a value that must stay as it was, write the snapshot out: const first = untrack(() => props.x).
  • Computing in the body runs once: const n = items.length + 1, const a = props.a ?? 1 or makeChart(items) is W001. Use derived, watch, JSX or a handler.
  • Array patterns and computed keys (const [a] = props.pair, const { [k]: a } = props) are W001: read them at the use site.
  • const stays read-only: assigning to a const alias is W001, which says to use let or props.x =.
  • state(prop) never follows the prop (W005). Use state.linked(() => prop) for an editable local copy.
  • Pass a box (value={coupon}) only when the child must write it; otherwise pass .value.

Writing a prop

A component can write its own props. let makes a local writable; +=, ++ and let { a } = props work too.

In the childParent passed n={x}Parent passed bind:n={box}
Read n or props.nLiveLive
let n = props.n; n = 5Holds in the child until the parent's value next changes, then the parent winsWrites box
props.n = 5The sameThe same: writes box
const n = props.n; n = 5W001: use let or props.n =W001

bind: at the call site alone decides whether a write leaves the component. <Card n={box.value} /> never writes box, whatever Card does. It takes a box, on a component (W061 otherwise). weft tsc and the editor read bind:n={box} as n with the box's value, so it satisfies a required prop and a wrong type is an error. Plain tsc can't connect bind:n to n and reports the prop as missing: run weft tsc. n={bind(box)} is the same binding in a form plain tsc accepts. An island's props arrive as data, so a write there is always local.

Only a whole prop can be written this way. let n = props.user.name; n = 1 is W001; props.user.name = 1 writes that object, as in any JavaScript.