Skip to content

[live_ui] build_render_assigns hardcodes __changed__: %{} — slot comprehensions don't re-evaluate on widget update #160

Description

@ty13r

[live_ui] build_render_assigns hardcodes __changed__: %{} — slot comprehensions don't re-evaluate when a widget's live_component updates

Summary

LiveUi.Widget.build_render_assigns/1 sets __changed__: %{} (Phoenix: "no wrapper assigns changed") in the render assigns passed to wrapper modules. The wrapper is invoked manually (@widget_wrapper_module.render(@render_assigns)), so Phoenix doesn't rebuild change-tracking for that call — %{} suppresses re-evaluation of HEEx for comprehensions inside render_slot(@inner_block) when the widget's live_component receives update/2 with new slot content. A conditionally-added child (e.g. an active-dispatch indicator appended after a PubSub broadcast) is never mounted.

Evidence (verified via Codex deep-review against Phoenix 1.1.27 source)

  • packages/live_ui/lib/live_ui/widget.exbuild_render_assignsMap.put(:__changed__, %{}).
  • Slot path: LiveUi.Renderer (renderer.ex:200) passes a for child <- child_elements(...) comprehension slot into SidebarSection.component, which calls render_slot(@inner_block) (sidebar_section.ex:82).
  • Phoenix 1.1.27: %{} = unchanged, nil = changed (utils.ex:132, engine.ex:1399); slot fns receive the caller's __changed__ (phoenix_component.ex:1137).
  • Downstream failing case (ariston-ui): operator_surface_live_test.exs MAP-3 active-dispatch indicator — currently @tag :skip pending this fix.

Candidate hotfix (correct but blunt — NOT the recommended fix)

Branch The-Metagraph/ash_ui:claude/live-ui-changed-nil flips %{}nil (always-changed / first-mount equivalent). Codex review: correct + safer than %{}, no correctness regression (phx-update="ignore" intact, streams unaffected, stable LiveComponent IDs prevent duplicate mounts), BUT it defeats wrapper-level diff minimization — meaningful extra server CPU + larger diffs on large composed trees (sidebars/rails/tables; ~69 modules use LiveUi.Component). Negligible for simple widgets.

Recommended architect fix

Don't hardcode either extreme. In build_render_assigns/1: preserve/thread the ACTUAL wrapper-level __changed__ when present (it's available in the assigns LiveUi.Component.component/1 copies into widget_assigns, component.ex:275), fall back to nil only when absent, and mark derived :metadata changed when wrapper/identity metadata (esp. inner_block) changes. Add a focused LiveView regression test: a SidebarSection receives updated slot children → the new child component mounts.

Ariston-side status

MAP-3 test is @tag :skip with a ref to this issue + the candidate branch, so the suite stays honest. Un-skip once the framework fix lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions