Svelte / Basic: reactive components and ownership / Lesson 3
Props, callbacks and state ownership
Define who owns a value and use explicit callbacks or deliberate bindings instead of mutating another component’s state accidentally.
On this page
Lesson goals
- Explain $props and callback contracts with current values.
- Distinguish a binding opt-in from permission to mutate arbitrary parent objects.
Props carry an interface, not unrestricted mutation rights
A parent passes props to describe a child’s input and callback capabilities. $props declares that boundary in runes mode. A child can display current input and request a change through a callback without taking ownership of the parent’s entire data object. Mutating a proxy supplied by another component can produce ownership warnings and surprising shared state. A warning is a design signal, not something to silence by marking every property bindable. Choose one clear owner for the source value and specify which operations another component may request. A component boundary does not create server authorization for those requests.
Request an intent through an explicit callback prop
The child receives a title and an onrename callback, then passes the proposed text when clicked. Its callback name is an application API, not a built-in security feature. The parent owns validation, pending state and any server action. The fragment is uncompiled and does not show a real input control; it isolates the communication boundary. In Svelte 5, callback props provide a direct alternative to older component-event patterns. Use current documented event attributes such as onclick for DOM events, while keeping component callback names consistent and typed where appropriate.
<script>
let { title, onrename } = $props();
</script>
<h2>{title}</h2>
<button onclick={() => onrename("Revised course title")}>Suggest title</button>
The child requests a change and does not implement persistence. No component event or callback was executed.
Make two-way data flow a deliberate opt-in
$bindable identifies a prop that supports the component binding contract. It is useful for carefully designed controls but does not justify exposing every nested property for mutation. Define defaults and parent/child responsibilities explicitly, and verify how fallback values behave rather than assuming a default object became a reactive proxy. A local prop override and mutation of a shared nested object have different effects. Keep unidirectional callback flow when it makes intent clearer, and use binding when the control’s public API truly benefits from it. Neither pattern replaces runtime validation at a server boundary.
Test a narrow intent payload without shared mutation
The local function returns an immutable rename intent containing the course ID and normalized title. The source record remains unchanged. This is a domain message model, not a Svelte callback or bindable test. It shows why a child need not receive a mutable persistence entity just to request one edit. Validate nontext and length constraints separately in the real input boundary, and authorize the current principal before the actual write. A trusted-looking component event can be forged by a caller and should not become a direct unrestricted repository command.
function renameIntent(record,title) {return {type:"rename-course",id:record.id,title:title.trim()};}
const source={id:"c1",title:"Original"};
const intent=renameIntent(source," Revised ");
console.assert(intent.id==="c1" && intent.title==="Revised");
console.assert(source.title==="Original" && intent!==source);
A new intent is built and the source stays unchanged. No callback binding, validation library or server operation is tested.
Preserve current props and canonical server results
A copied value can become stale when the parent replaces its input. Read props through the intended reactive contract, and only create local editable state when that separation is deliberate. Reconcile server normalization, revision conflicts and rejected edits rather than assuming a callback invocation means success. Pending UI may reduce accidental double clicks but cannot guarantee one durable effect. Test parent replacement, child interaction, binding opt-in and rejection paths in an authorized component/browser integration. This course executes the intent function only; ownership warnings, $props updates, $bindable behavior and DOM events remain unexecuted. Keep those limits clear in reports.
Common errors and fixes
A child mutates a parent-owned proxy unexpectedly
- Why it happens
- Readable input was treated as unrestricted ownership.
- How to fix it
- Use a callback intent or a deliberately declared binding contract.
- Prevent it next time
- Test parent state after every supported child interaction.
A copied prop ignores later parent changes
- Why it happens
- A snapshot was mistaken for ongoing reactive input.
- How to fix it
- Preserve current prop access or document intentional local draft state.
- Prevent it next time
- Include parent replacement and draft-reset fixtures.
A callback is reported as a successful save
- Why it happens
- UI intent was confused with confirmed domain outcome.
- How to fix it
- Track pending/rejected/canonical results separately.
- Prevent it next time
- Test rejected edits and source conflicts as well as clicks.
Check your understanding
1. Does $bindable authorize a child to update the database?
Show worked solution
No. It declares a component data-flow capability, not caller authority or persistence. The actual server entry must validate values, resolve current identity and enforce object policy. Keep component ownership and server authorization as separate contracts even when the control uses convenient two-way binding.
2. Why return a rename intent instead of giving the child a mutable entity?
Show worked solution
A narrow intent expresses the one requested operation and avoids exposing unrelated mutable fields. It is easy to test and route through validation, pending state and a shared use case. The intent still needs runtime checks and authorization; its small shape does not make it trusted automatically.
Key takeaways
- Props, callbacks and bindings need explicit state ownership.
- Component intent and confirmed persistence are different outcomes.