Skip to content

An auto-props control edits every variant of the story, not the one it belongs to #473

Description

@50rayn

Editing an auto-props control on one variant changes the others. The control belongs to one component in one variant, and _hPropState is keyed that way, but the value reaches every variant in the story.

Found while bringing auto-props to Svelte (#233) — the Svelte implementation does not do this, which is how it surfaced.

Reproduce

conformance-auto-props in examples/vue3 (or nuxt4), grid layout, two variants:

  • naked<AutoStateProps />, no bindings, no initState
  • stateinitState returning { name: 'Fry' }, bound with :name="state.name"

Open ?variantId=naked and set the name control under <AutoStateProps> to Bender:

before   ["Hello world!", "Hello Fry!"]
after    ["Hello Bender!", "Hello Bender!"]

The state variant has its own initState and its own binding, and neither survives an edit made on a different variant.

Cause

implicitState is created once per story — Story.ts:39 — and every variant's state is two-way synced with it:

mountStateSync = syncStateBundledAndExternal(mountVariant.value.state, implicitState())

Variant.ts:86 runs that for every variant in mount mode, not only the current one. The render-mode sibling at :164 is guarded by renderContext.currentVariant?.id === variant.id; the mount one has no such guard, because sharing story state across variants is the intended behaviour there.

_hPropState then rides the same channel. It is not story state — it is per-variant, per-component-index bookkeeping that only means anything next to the _hPropDefs it was recorded against — so two variants that both render a component at index 0 overwrite each other.

Worth deciding

Excluding the auto-props bookkeeping from that sync looks right: _hPropState and _hPropDefs are addressed by a component index that is only meaningful within one variant, so sharing them across variants cannot be correct even when it looks harmless.

There is precedent for treating them as not-story-state: toPresetState already omits _hPropDefs when storing a preset (packages/poveste-app/src/app/util/state.ts:62), and StatePresets.vue carries the same omit list. This would be a third place naming those keys, which suggests the list belongs somewhere shared rather than being written out again.

What it must not break is the behaviour the mount-mode sync exists for: a variant with no initState picking up the story's own state.

Coverage

Nothing catches it. e2e/auto-props.spec.ts runs on all four books and deliberately asserts no isolation, with a comment saying why — the two plugins disagree, and the spec could not claim a shared contract that does not exist. Once this is fixed, that exclusion is what should be deleted, and the assertion belongs there: edit one variant's control, the sibling is untouched.

Related: #233, #431.

Metadata

Metadata

Assignees

No one assigned

    Labels

    a:appThe book UI — chrome, sandbox, controls panea:pluginsPlugin API and official pluginsa:vueVue plugin and Vue story surfacesprint:4-bugsIndependent correctness bugs; runs in parallel with any sprint

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions