LearnManaging state
Sharing state between components
Sometimes two components need to change together. Keep the state in one place, their closest common parent, and pass it down to both. This is called lifting state up, and it works in weft the same way it does in React, with one extra tool: bind:, which lets a child write to its parent's state without a callback.
Lifting state up by example
Here a parent shows two panels. Only one should be open at a time: opening the second closes the first.
Lisbon is Portugal's capital, built on seven hills above the Tagus estuary.
import { state } from '@weft-js/core'import { Button, Card } from '@weft-js/warp'function Panel(props: { title: string; active: boolean; onShow: () => void; children?: unknown }) { return ( <Card title={props.title}> {props.active ? ( <p>{props.children}</p> ) : ( <Button size="sm" emphasis="ghost" onClick={props.onShow}>Show</Button> )} </Card> )}export default function Lisbon() { // The parent owns which panel is open, so at most one ever is. const open = state(0) return ( <> <Panel title="About" active={open.value === 0} onShow={() => (open.value = 0)}> Lisbon is Portugal's capital, built on seven hills above the Tagus estuary. </Panel> <Panel title="Getting around" active={open.value === 1} onShow={() => (open.value = 1)}> Old yellow trams climb streets too steep for buses. </Panel> </> )}Try this: Open the second panel. The first closes, because the parent holds which one is open.
If each panel kept its own open box, they couldn't know about each other. Opening one would leave the other open too. To coordinate them, take three steps.
Step 1: remove the state from the children
A panel that owns its own state looks like this:
import { state } from '@weft-js/core'export function Panel(props: { title: string; children?: unknown }) { const open = state(false) return ( <section> <h3>{props.title}</h3> {open.value ? <p>{props.children}</p> : <button type="button" onClick={() => (open.value = true)}>Show</button>} </section> )}Replace the box with a prop that the parent sets: active: boolean. The panel no longer decides whether it's open.
Step 2: pass the value from the parent
The parent now says which panel is active. Try it with a constant first: <Panel active={true}> and <Panel active={false}>. Each panel shows what it's told.
Step 3: add state to the parent
Lifting state up often changes what the state is. Two booleans become one number: the index of the open panel. The parent holds it, and gives each panel a way to ask to be opened:
import { state } from '@weft-js/core'function Panel(props: { title: string; active: boolean; onShow: () => void; children?: unknown }) { return ( <section> <h3>{props.title}</h3> {props.active ? <p>{props.children}</p> : <button type="button" onClick={props.onShow}>Show</button>} </section> )}export function Accordion() { const open = state(0) return ( <> <Panel title="About" active={open.value === 0} onShow={() => (open.value = 0)}>Seven hills.</Panel> <Panel title="Getting around" active={open.value === 1} onShow={() => (open.value = 1)}>Yellow trams.</Panel> </> )}open.value === 0 is an expression in JSX, so it's live: when open changes, each panel's active prop follows, and only the parts of each panel that read props.active update. Neither Panel runs again.
Letting a child write: bind:
The accordion passes a callback, onShow, which is often right: the child reports an event, and the parent decides what it means. When the child simply edits a value, there's a shorter way. The parent passes its box with bind:, and the child writes its prop as if it were its own:
import { state } from '@weft-js/core'function Stepper(props: { value?: number }) { let { value = 1 } = props return <button type="button" onClick={() => value++}>{value}</button>}function Summary(props: { qty: number }) { return <p>{props.qty} in the basket</p>}export default function Checkout() { const qty = state(1) return ( <> <Stepper bind:value={qty} /> <Summary qty={qty.value} /> </> )}Stepper and Summary never talk to each other. They share qty because Checkout owns it.
let { value = 1 } = props makes a local you can write. Without bind:, a write stays inside Stepper until the parent passes a new value. With bind:value={qty} at the call site, the write goes to qty, and Summary follows. The child is the same in both cases; only the parent decides whether its writes leave. Writing to a parent with bind: covers it in full.
Deep diveControlled and uncontrolled, in weft
React distinguishes controlled components, driven by props, from uncontrolled ones, which keep their own state, and a component author usually picks one.
In weft one component can be both. Stepper above keeps its own value when used alone (<Stepper />), starts from the parent's value and then goes its own way when given one (<Stepper value={2} />), and is fully controlled when bound (<Stepper bind:value={qty} />). The author writes it once, with let, and the caller chooses.
Passing a box
You can also pass the box itself, typed State<T>:
import { state, type State } from '@weft-js/core'function Coupon(props: { code: State<string> }) { return <input aria-label="Coupon" value={props.code.value} onInput={e => (props.code.value = e.currentTarget.value)} />}export function Pay() { const code = state('') return ( <form> <Coupon code={code} /> <p>{code.value ? `Using ${code.value}` : 'No coupon'}</p> </form> )}Pass the box when the child needs the box itself: to read one entry with .get (see Keyed state and entries), or to hand it on further. For a child that edits one value, bind: keeps the child simpler, because it works with or without a parent box.
A single source of truth for each piece of state
In a weft app, each piece of state has one owner. Some state lives in a leaf component, like whether a tooltip is open. Some lives near the top, like which page you're on. A few things live in a module or a