From 3035f11e3097e157cfa1da0a98106819bf8e9a78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:26:56 +0000 Subject: [PATCH 1/5] Initial plan From 9fa769aa0b7e8e5950655dade21af25c1f94a2cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:44:43 +0000 Subject: [PATCH 2/5] Abort render when re-render is requested during render, bump version to 2.3.1 Co-authored-by: L3P3 <4629449+L3P3@users.noreply.github.com> --- package.json | 2 +- src/lui.js | 23 ++++++++++++++--------- test/hook-state.test.js | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 9d6abfe..4066fd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lui", - "version": "2.3.0", + "version": "2.3.1", "description": "web framework", "homepage": "https://l3p3.de/dok/lui.html", "repository": { diff --git a/src/lui.js b/src/lui.js index 344e1ff..7987889 100644 --- a/src/lui.js +++ b/src/lui.js @@ -686,6 +686,7 @@ const instance_render = (dom_parent, dom_first) => { DEBUG && thrown !== dom_cache ) throw thrown; + if (instance.dirty) return; } const {dom} = instance; @@ -1475,10 +1476,11 @@ export const hook_state = initial => { EXTENDED ? instance_current_get(current_slots_) : current_ ), value); - slot[0] !== value && ( - slot[0] = value, - EXTENDED ? dirtify_slots(current_slots_) : dirtify_instance(current_) - ); + if (slot[0] !== value) { + slot[0] = value; + EXTENDED ? dirtify_slots(current_slots_) : dirtify_instance(current_); + if (EXTENDED ? current_slots === current_slots_ : current === current_) throw dom_cache; + } return value; }, () => slot[0] @@ -1756,6 +1758,7 @@ export const hook_sub = (getter, deps) => { DEBUG && thrown !== dom_cache ) throw thrown; + if (current.dirty) throw thrown; } // context pop @@ -1930,6 +1933,7 @@ export const hook_map = (getter, list_data, deps) => { DEBUG && thrown !== dom_cache ) throw thrown; + if (current.dirty) throw thrown; } DEBUG && ( @@ -2063,12 +2067,13 @@ export const hook_model = mutations => { ? callback_wrap(mutations[key], [slot[0], ...args], stack + ' -> #' + key) : (0, mutations[key])(slot[0], ...args) ); - slot[0] !== value && ( + if (slot[0] !== value) { DEBUG && - callback_wrap(state_check, [value], stack + ' -> #' + key), - slot[0] = value, - EXTENDED ? dirtify_slots(current_slots_) : dirtify_instance(current_) - ); + callback_wrap(state_check, [value], stack + ' -> #' + key); + slot[0] = value; + EXTENDED ? dirtify_slots(current_slots_) : dirtify_instance(current_); + if (EXTENDED ? current_slots === current_slots_ : current === current_) throw dom_cache; + } return value; }; } diff --git a/test/hook-state.test.js b/test/hook-state.test.js index 4a585bc..02e303f 100644 --- a/test/hook-state.test.js +++ b/test/hook-state.test.js @@ -33,3 +33,28 @@ test('hook_state: can manage component state', () => { expect(h1.textContent).toBe('Count: 1'); }); + +test('hook_state: aborts render and re-renders when setter called during render', () => { + const root = root_create(); + let render_count = 0; + + init(() => { + render_count++; + const [value, value_set] = hook_state(false); + + // Setting the same value should NOT trigger an abort or extra render + if (!value) value_set(true); + + return [ + node_dom('p', { + textContent: String(value), + }), + ]; + }, root); + + // The component should have rendered twice: + // 1st render: value=false, setter called with true → aborts and re-renders + // 2nd render: value=true, setter not called again → stable + expect(render_count).toBe(2); + expect(root.querySelector('p').textContent).toBe('true'); +}); From 85b2e9a756304080d07eb2669f05d408fd28fac3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:29:41 +0000 Subject: [PATCH 3/5] Address review feedback: merge catch ifs, 3-line if format, improve test counter Co-authored-by: L3P3 <4629449+L3P3@users.noreply.github.com> --- src/lui.js | 12 ++++++++---- test/hook-state.test.js | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/lui.js b/src/lui.js index 7987889..70bc027 100644 --- a/src/lui.js +++ b/src/lui.js @@ -1479,7 +1479,9 @@ export const hook_state = initial => { if (slot[0] !== value) { slot[0] = value; EXTENDED ? dirtify_slots(current_slots_) : dirtify_instance(current_); - if (EXTENDED ? current_slots === current_slots_ : current === current_) throw dom_cache; + if ( + EXTENDED ? current_slots === current_slots_ : current === current_ + ) throw dom_cache; } return value; }, @@ -1757,8 +1759,8 @@ export const hook_sub = (getter, deps) => { if ( DEBUG && thrown !== dom_cache + || current.dirty ) throw thrown; - if (current.dirty) throw thrown; } // context pop @@ -1932,8 +1934,8 @@ export const hook_map = (getter, list_data, deps) => { if ( DEBUG && thrown !== dom_cache + || current.dirty ) throw thrown; - if (current.dirty) throw thrown; } DEBUG && ( @@ -2072,7 +2074,9 @@ export const hook_model = mutations => { callback_wrap(state_check, [value], stack + ' -> #' + key); slot[0] = value; EXTENDED ? dirtify_slots(current_slots_) : dirtify_instance(current_); - if (EXTENDED ? current_slots === current_slots_ : current === current_) throw dom_cache; + if ( + EXTENDED ? current_slots === current_slots_ : current === current_ + ) throw dom_cache; } return value; }; diff --git a/test/hook-state.test.js b/test/hook-state.test.js index 02e303f..88b70a7 100644 --- a/test/hook-state.test.js +++ b/test/hook-state.test.js @@ -37,14 +37,16 @@ test('hook_state: can manage component state', () => { test('hook_state: aborts render and re-renders when setter called during render', () => { const root = root_create(); let render_count = 0; + let after_set_count = 0; init(() => { render_count++; const [value, value_set] = hook_state(false); - // Setting the same value should NOT trigger an abort or extra render if (!value) value_set(true); + after_set_count++; + return [ node_dom('p', { textContent: String(value), @@ -56,5 +58,7 @@ test('hook_state: aborts render and re-renders when setter called during render' // 1st render: value=false, setter called with true → aborts and re-renders // 2nd render: value=true, setter not called again → stable expect(render_count).toBe(2); + // Code after the setter was only reached once (the 2nd render, since the 1st aborted) + expect(after_set_count).toBe(1); expect(root.querySelector('p').textContent).toBe('true'); }); From 1dbb46308fde3b13344ae2c4013588dbae00be40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:31:27 +0000 Subject: [PATCH 4/5] Fix operator placement: trailing || to match codebase style Co-authored-by: L3P3 <4629449+L3P3@users.noreply.github.com> --- src/lui.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lui.js b/src/lui.js index 70bc027..9f38b22 100644 --- a/src/lui.js +++ b/src/lui.js @@ -1758,8 +1758,8 @@ export const hook_sub = (getter, deps) => { catch (thrown) { if ( DEBUG && - thrown !== dom_cache - || current.dirty + thrown !== dom_cache || + current.dirty ) throw thrown; } @@ -1933,8 +1933,8 @@ export const hook_map = (getter, list_data, deps) => { catch (thrown) { if ( DEBUG && - thrown !== dom_cache - || current.dirty + thrown !== dom_cache || + current.dirty ) throw thrown; } From 671f49473c499f2beadde9f23bb3af3ad256de21 Mon Sep 17 00:00:00 2001 From: Len Date: Wed, 4 Mar 2026 17:41:57 +0100 Subject: [PATCH 5/5] formatting nitpick --- src/lui.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lui.js b/src/lui.js index 9f38b22..1be5a55 100644 --- a/src/lui.js +++ b/src/lui.js @@ -1480,7 +1480,9 @@ export const hook_state = initial => { slot[0] = value; EXTENDED ? dirtify_slots(current_slots_) : dirtify_instance(current_); if ( - EXTENDED ? current_slots === current_slots_ : current === current_ + EXTENDED + ? current_slots === current_slots_ + : current === current_ ) throw dom_cache; } return value; @@ -2075,7 +2077,9 @@ export const hook_model = mutations => { slot[0] = value; EXTENDED ? dirtify_slots(current_slots_) : dirtify_instance(current_); if ( - EXTENDED ? current_slots === current_slots_ : current === current_ + EXTENDED + ? current_slots === current_slots_ + : current === current_ ) throw dom_cache; } return value;