[Svelte] Updating Props after the fact #2757
-
Hey guys, I have a question regarding reactivity and props in zag.js using svelte. I am building a component library but I am running into strange bugs. First let's take a look at the example code for the switch component (1:1 copy from the docs): <script lang="ts">
import { normalizeProps, useMachine } from "@zag-js/svelte"
import * as zagSwitch from "@zag-js/switch"
const id = $props.id()
const service = useMachine(zagSwitch.machine, {
id,
name: "switch",
})
const api = $derived(zagSwitch.connect(service, normalizeProps))
</script>
<label {...api.getRootProps()}>
<input {...api.getHiddenInputProps()} />
<span {...api.getControlProps()}>
<span {...api.getThumbProps()}></span>
</span>
<span {...api.getLabelProps()}>{api.checked ? "On" : "Off"}</span>
</label> Notice how there is no derived rune on the useMachine-Statement, meaning it will not reflect changes to the props. Imagine this use case:I have a form in which you have multiple fields like a select and a switch. Selecting option A from the select should prevent users from switching the switch to on, so I would like to disable it depending on a state-rune. <Switch disabled={disableSwitch} /> Now for my implementation of the switch (I called it toggle and created a component for each <script lang="ts">
import * as zagSwitch from '@zag-js/switch';
import { cn } from '../../utils/classes.utility';
import Control from './control.component.svelte';
import Label from './label.component.svelte';
import { setRootContext } from './root.context';
let {
id: customId,
label,
checked = $bindable(),
value = $bindable(),
color = 'primary',
class: classProp,
children,
...restProps
}: RootProps = $props();
const id = $props.id();
const [machineProps, rootProps] = $derived(
zagSwitch.splitProps({
...restProps,
id: customId ?? id
})
);
const service = $derived(
useMachine(zagSwitch.machine, {
...machineProps,
value,
get checked() {
return checked;
},
onCheckedChange(details) {
checked = details.checked;
machineProps.onCheckedChange?.(details);
}
})
);
const api = $derived(zagSwitch.connect(service, normalizeProps));
setRootContext(() => ({ api, color }));
const rootClasses = $derived(
cn(
'group flex w-fit cursor-pointer items-center gap-2',
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
classProp
)
);
</script>
<label {...api.getRootProps()} {...rootProps} class={rootClasses}>
<!-- HIDDEN INPUT -->
<input {...api.getHiddenInputProps()} />
{#if children}
{@render children()}
{:else}
<!-- DEFAULT -->
<Control />
<Label>{label}</Label>
{/if}
</label> Notice how I wrapped the EDIT: When I remove the Now when I use the So my question is, how am I supposed to implement reactive props and bindings properly? I am currently running version 1.24.2 of |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
Consulting Claude on this issue I came to the possible solution of putting my props inside a function and passing that to the machine: // Pass a function that returns the config - this makes it reactive
const service = useMachine(
zagSwitch.machine,
() => ({
...machineProps,
value,
checked,
onCheckedChange(details) {
checked = details.checked;
machineProps.onCheckedChange?.(details);
}
})
); |
Beta Was this translation helpful? Give feedback.
-
Hi! Yes, using a function for the context is the correct solution here. Let me explain why: The ProblemZag machines capture their context once during initialization. When you pass a static object to The SolutionBy passing a function that returns the context, you're enabling the machine to receive fresh values on each evaluation: const service = useMachine(zagSwitch.machine, () => ({
...machineProps,
value,
checked,
onCheckedChange(details) {
checked = details.checked
machineProps.onCheckedChange?.(details)
},
})) This works because:
This approach ensures single-directional data flow while maintaining reactivity. Your solution is on the right track! |
Beta Was this translation helpful? Give feedback.
-
Thanks for your answer. This should be updated in the documentation, I took the solution that is being presented for the checkbox and ported it over to the switch component. https://zagjs.com/components/svelte/checkbox#controlled-checkbox I will look into updating the docs, if I find some time to do so. |
Beta Was this translation helpful? Give feedback.
Hi! Yes, using a function for the context is the correct solution here. Let me explain why:
The Problem
Zag machines capture their context once during initialization. When you pass a static object to
useMachine
, themachine takes a snapshot of those values and doesn't automatically react to prop changes.
The Solution
By passing a function that returns the context, you're enabling the machine to receive fresh values on each evaluation:
This works because: