Skip to content

Commit 868e0c6

Browse files
committed
fix bug: call do_after_render callbacks during nested rerenders
1 parent dc63760 commit 868e0c6

2 files changed

Lines changed: 47 additions & 4 deletions

File tree

lua/morph.lua

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,14 +1177,16 @@ function Morph:mount(tree)
11771177

11781178
--- Perform a full re-render of the component tree.
11791179
rerender = function()
1180-
after_render_callbacks = {}
1181-
11821180
local simplified_tree = reconcile_tree(self.component_tree.old, tree)
11831181
self.component_tree.old = tree
11841182
self:render(simplified_tree)
11851183

1186-
-- Run any scheduled after-render callbacks
1187-
for _, callback in ipairs(after_render_callbacks) do
1184+
-- Run any scheduled after-render callbacks, then clear the list.
1185+
-- We clear after (not before) to handle the case where ctx:update()
1186+
-- is called during the update phase, which would trigger a nested rerender.
1187+
local callbacks = after_render_callbacks
1188+
after_render_callbacks = {}
1189+
for _, callback in ipairs(callbacks) do
11881190
callback()
11891191
end
11901192
end

spec/morph_spec.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,6 +2122,47 @@ describe('Morph', function()
21222122
end)
21232123
end)
21242124

2125+
it(
2126+
'executes do_after_render callbacks even when update is called during update phase',
2127+
function()
2128+
with_buf({}, function()
2129+
local callback_executed = false
2130+
local capture_ctx
2131+
2132+
--- @param ctx morph.Ctx<{}, { open: boolean, updated: boolean }>
2133+
local function ComponentWithCallback(ctx)
2134+
if ctx.phase == 'mount' then
2135+
ctx.state = { open = false, updated = false }
2136+
capture_ctx = ctx
2137+
end
2138+
-- Only schedule callback and call update on the first update when open becomes true
2139+
if ctx.state.open and ctx.phase == 'update' and not ctx.state.updated then
2140+
ctx:do_after_render(function() callback_executed = true end)
2141+
-- Mark as updated to prevent infinite loop
2142+
ctx.state.updated = true
2143+
ctx:update(ctx.state)
2144+
end
2145+
return { ctx.state.open and 'open' or 'closed' }
2146+
end
2147+
2148+
local r = Morph.new(0)
2149+
r:mount(h(ComponentWithCallback))
2150+
assert.are.same('closed', get_text())
2151+
assert.is_false(callback_executed)
2152+
2153+
-- Now trigger the update that causes the issue
2154+
capture_ctx.state.open = true
2155+
capture_ctx:refresh()
2156+
2157+
-- The callback should have been executed
2158+
assert.is_true(
2159+
callback_executed,
2160+
'do_after_render callback should be executed even when ctx:update is called during update phase'
2161+
)
2162+
end)
2163+
end
2164+
)
2165+
21252166
it('does not re-render when update called during mount phase', function()
21262167
with_buf({}, function()
21272168
local render_count = 0

0 commit comments

Comments
 (0)