13
238
Fork
You've already forked app
18

Document canonical place to initialise state-maintaining child components. Was: infinite loop when component calls this.page.update() #311

Open
opened 2026年02月25日 05:40:24 +01:00 by wunter8 · 4 comments

I found a bug when I was working on a project in Kitten.

If a kitten.Component on a kitten.Page calls this.page.update(), it starts an infinite loop.

AI helped me track down the exact issue, proposed a fix, and created the minimal example in the attached file.

Let me know if you have any questions or want me to provide additional information.

I found a bug when I was working on a project in Kitten. If a kitten.Component on a kitten.Page calls `this.page.update()`, it starts an infinite loop. AI helped me track down the exact issue, proposed a fix, and created the minimal example in the attached file. Let me know if you have any questions or want me to provide additional information.
Author
Copy link

Here are the main files in the tarball:

index.page.js

import kitten from '@small-web/kitten'
import Child from './Child.component.js'
export default class IndexPage extends kitten.Page {
 html() {
 // Before the fix, this count grows every time 'update' is called
 // because children are never cleared.
 console.log('Total children in Page:', this.children.length)
 if (this.children.length > 500) {
 return
 }
 return kitten.html`
 <main>
 <h1>Kitten Loop Reproduction</h1>
 <${Child.connectedTo(this)} />
 </main>
 `
 }
}

Child.component.js

import kitten from '@small-web/kitten'
export default class Child extends kitten.Component {
 html() {
 return kitten.html`
 <div id="${this.id}">
 <p>Child ID: ${this.id}</p>
 <button name="triggerLoop" connect>Trigger Loop</button>
 </div>
 `
 }
 onTriggerLoop() {
 console.log('Handler triggered on child:', this.id)
 // 1. This triggers a page update.
 // 2. Page.update() calls Page.html().
 // 3. Page.html() calls Child.connectedTo(this), adding a NEW child to the array.
 // 4. Because bubbleEvent iterates the live array, it eventually hits the new child.
 // 5. The new child runs onTriggerLoop(), starting the cycle again.
 this.page.update()
 }
}

fix-infinite-loop.patch

diff --git a/src/lib/KittenComponent.js b/src/lib/KittenComponent.js
index 0ff29fe9..15ea1cea 100644
--- a/src/lib/KittenComponent.js
+++ b/src/lib/KittenComponent.js
@@ -327,6 +327,7 @@ class ${this.constructor.name} extends kitten.Page {
 for (const child of this.children) {
 child.bubbleEvent('_onDisconnect')
 }
+ this.children = []
 this.page.send(await globalThis.kitten.html`<${this.component} />`)
 }
 
@@ -366,7 +367,11 @@ class ${this.constructor.name} extends kitten.Page {
 }
 }
 // Bubble event to child nodes.
- for (const child of this.children) {
+ // We iterate over a copy of the children array because handlers might
+ // trigger updates that result in new children being added to the
+ // component hierarchy, which would lead to an infinite loop.
+ const children = [...this.children]
+ for (const child of children) {
 child.bubbleEvent(eventHandlerName, data, event, target)
 }
 }
Here are the main files in the tarball: index.page.js ``` import kitten from '@small-web/kitten' import Child from './Child.component.js' export default class IndexPage extends kitten.Page { html() { // Before the fix, this count grows every time 'update' is called // because children are never cleared. console.log('Total children in Page:', this.children.length) if (this.children.length > 500) { return } return kitten.html` <main> <h1>Kitten Loop Reproduction</h1> <${Child.connectedTo(this)} /> </main> ` } } ``` Child.component.js ``` import kitten from '@small-web/kitten' export default class Child extends kitten.Component { html() { return kitten.html` <div id="${this.id}"> <p>Child ID: ${this.id}</p> <button name="triggerLoop" connect>Trigger Loop</button> </div> ` } onTriggerLoop() { console.log('Handler triggered on child:', this.id) // 1. This triggers a page update. // 2. Page.update() calls Page.html(). // 3. Page.html() calls Child.connectedTo(this), adding a NEW child to the array. // 4. Because bubbleEvent iterates the live array, it eventually hits the new child. // 5. The new child runs onTriggerLoop(), starting the cycle again. this.page.update() } } ``` fix-infinite-loop.patch ``` diff --git a/src/lib/KittenComponent.js b/src/lib/KittenComponent.js index 0ff29fe9..15ea1cea 100644 --- a/src/lib/KittenComponent.js +++ b/src/lib/KittenComponent.js @@ -327,6 +327,7 @@ class ${this.constructor.name} extends kitten.Page { for (const child of this.children) { child.bubbleEvent('_onDisconnect') } + this.children = [] this.page.send(await globalThis.kitten.html`<${this.component} />`) } @@ -366,7 +367,11 @@ class ${this.constructor.name} extends kitten.Page { } } // Bubble event to child nodes. - for (const child of this.children) { + // We iterate over a copy of the children array because handlers might + // trigger updates that result in new children being added to the + // component hierarchy, which would lead to an infinite loop. + const children = [...this.children] + for (const child of children) { child.bubbleEvent(eventHandlerName, data, event, target) } } ```
Owner
Copy link

Hey @wunter8, I think Kitten is actually working as designed here but the correct way of using child components in class-based components hasn’t really been documented.

To fix your example, create the child component conditionally, based on whether it exists or not:

import kitten from '@small-web/kitten'
import Child from './Child.component.js'
export default class IndexPage extends kitten.Page {
 html() {
 this.Child ??= Child.connectedTo(this)
 // Before the fix, this count grows every time 'update' is called
 // because children are never cleared.
 console.log('Total children in Page:', this.children.length)
 if (this.children.length > 500) {
 return
 }
 return kitten.html`
 <main>
 <h1>Kitten Loop Reproduction</h1>
 <${this.Child} />
 </main>
 `
 }
}

If you’re ok with it too, I’ll modify this issue to be a documentation issue. (The class-based workflow really isn’t documented properly and I’m still discovering how best to use it myself.) :)

Hey @wunter8, I think Kitten is actually working as designed here but the correct way of using child components in class-based components hasn’t really been documented. To fix your example, create the child component conditionally, based on whether it exists or not: ```js import kitten from '@small-web/kitten' import Child from './Child.component.js' export default class IndexPage extends kitten.Page { html() { this.Child ??= Child.connectedTo(this) // Before the fix, this count grows every time 'update' is called // because children are never cleared. console.log('Total children in Page:', this.children.length) if (this.children.length > 500) { return } return kitten.html` <main> <h1>Kitten Loop Reproduction</h1> <${this.Child} /> </main> ` } } ``` If you’re ok with it too, I’ll modify this issue to be a documentation issue. (The class-based workflow really isn’t documented properly and I’m still discovering how best to use it myself.) :)
Author
Copy link

Oh, interesting! I haven't seen that pattern used anywhere (I've been looking at your other repos (like the Gaza Verified site) for examples), but I'll go update my code to do it like this instead.

Typescript is complaining that Property 'Child' does not exist on type 'IndexPage', but I can //@ts-ignore that.

I saw somewhere that class-based components cause extra performance overhead and should be used sparingly. Is that still the case? Because at the same time it seems like they are the new, "preferred" way of using Kitten.

In any case, changing this to a documentation issue is totally fine. Thanks for pointing me in the right direction!

Oh, interesting! I haven't seen that pattern used anywhere (I've been looking at your other repos (like the Gaza Verified site) for examples), but I'll go update my code to do it like this instead. Typescript is complaining that `Property 'Child' does not exist on type 'IndexPage'`, but I can //@ts-ignore that. I saw somewhere that class-based components cause extra performance overhead and should be used sparingly. Is that still the case? Because at the same time it seems like they are the new, "preferred" way of using Kitten. In any case, changing this to a documentation issue is totally fine. Thanks for pointing me in the right direction!
Owner
Copy link

Oh, interesting! I haven't seen that pattern used anywhere (I've been looking at your other repos (like the Gaza Verified site) for examples), but I'll go update my code to do it like this instead.

Well, there’s a good reason for that... I only just realised that’s how we should be doing it ;)

(I started doing something similar recently using the onChange() handler to initialise child components in components themselves but that doesn’t fit the lifecycle of a page. So I think doing it here should be the canonical way.)

Typescript is complaining that Property 'Child' does not exist on type 'IndexPage', but I can //@ts-ignore that.

It’s a bit convoluted but you can type it as:

export default class IndexPage extends kitten.Page {
 /** @type {ReturnType<typeof kitten.Component['connectedTo']>} */
 Child
 ...
}

I also just opened an issue to export a friendlier type from @small-web/kitten: kitten/globals#12

In the meanwhile, you can, of course, use a typedef. e.g.,

import kitten from '@small-web/kitten'
import Child from './Child.component.js'
/** @typedef {ReturnType<typeof kitten.Component['connectedTo']>} ConnectedChild */
export default class IndexPage extends kitten.Page {
 /** @type {ConnectedChild} */
 Child
 ...
}

I saw somewhere that class-based components cause extra performance overhead and should be used sparingly. Is that still the case? Because at the same time it seems like they are the new, "preferred" way of using Kitten.

I’d say use it unless you expect lots of components on a page. e.g., don’t use it if you’re recreating the Draw Together example with all those pixels.

I’m going with not prematurely optimising as it is so much nicer to author with.

In any case, changing this to a documentation issue is totally fine. Thanks for pointing me in the right direction!

Perfect, will do that now :)

And thanks so much for raising the issue 💕

> Oh, interesting! I haven't seen that pattern used anywhere (I've been looking at your other repos (like the Gaza Verified site) for examples), but I'll go update my code to do it like this instead. Well, there’s a good reason for that... I only just realised that’s how we should be doing it ;) (I started doing something similar recently using the `onChange()` handler to initialise child components in components themselves but that doesn’t fit the lifecycle of a page. So I think doing it here should be the canonical way.) > > Typescript is complaining that `Property 'Child' does not exist on type 'IndexPage'`, but I can //@ts-ignore that. It’s a bit convoluted but you can type it as: ```js export default class IndexPage extends kitten.Page { /** @type {ReturnType<typeof kitten.Component['connectedTo']>} */ Child ... } ``` I also just opened an issue to export a friendlier type from @small-web/kitten: https://codeberg.org/kitten/globals/issues/12 In the meanwhile, you can, of course, use a `typedef`. e.g., ```js import kitten from '@small-web/kitten' import Child from './Child.component.js' /** @typedef {ReturnType<typeof kitten.Component['connectedTo']>} ConnectedChild */ export default class IndexPage extends kitten.Page { /** @type {ConnectedChild} */ Child ... } ``` > I saw somewhere that class-based components cause extra performance overhead and should be used sparingly. Is that still the case? Because at the same time it seems like they are the new, "preferred" way of using Kitten. I’d say use it unless you expect lots of components on a page. e.g., don’t use it if you’re recreating the Draw Together example with all those pixels. I’m going with not prematurely optimising as it is so much nicer to author with. > In any case, changing this to a documentation issue is totally fine. Thanks for pointing me in the right direction! Perfect, will do that now :) And thanks so much for raising the issue 💕
aral changed title from (削除) infinite loop when component calls this.page.update() (削除ここまで) to Document canonical place to initialise state-maintaining child components. Was: infinite loop when component calls this.page.update() 2026年03月02日 19:11:24 +01:00
Sign in to join this conversation.
No Branch/Tag specified
main
full-typescript
stateful-components-api-breaking-change
ip-address-support
improved-markdown-pages
markdown-consistency
proper-markdown-implementation-take-2
proper-markdown-implementation
fix-markdown-links
improved-component-model
middleware-refactor
implicit-event-handlers
event-scopes
regression-tests
access-stats
stats
connected-components
streaming-html-kitten-chat-examples
data-encapsulation
domain-migrations
better-slash-forwarding-in-urls
manual-app-updates
logs
kitten-scoped-database
only-inform-small-web-host-of-status-updates-if-being-deployed-by-it
only-ping-domain-on-first-deploy
markdown-pages
node-22
id-command
settings-refactor
show-uploads-in-settings
backup-and-restore
database-view-in-settings
prewarmed-servers
htmx-2
html-and-css-fragments
without-source-maps
only-use-kitten-process-manager-for-development-servers
parameter-objects
page-socket-routes
versioning-and-package-json-changes
evergreen-web
auto-updates
build-web-into-dist
two-databases-are-better-than-one
kitten-namespace
npm-ci
use-my-htmx-fork
errors-not-exits-wip
standardisation
strong-typing-and-tests
kitten-tag
page-tag
db-table-delete
npm-install-and-validation-issues-on-html
busboy-express
readline-under-cluster
development-libraries
slots-and-classes
named-slots
credits
dot-js
markdown-fix
safer-fields
validation
wrapping-the-renderer
xhtm
end-to-end-encrypted-chat-example
chokidar-to-watcher-migration
alpinejs
uri-cleanup
web-settings
cryptography
settings-app
development-mode
post
No results found.
Labels
Clear labels
documentation
Anything related to documentation
error messages
Issues related to Kitten’s error message handling
examples
Related to apps in the examples folder
housekeeping
Improves code quality by refactoring redundancy, removing old code, etc.
low priority
A non-essential nice-to-have
needs design
Not a simple fix but something that requires architectural work
parser
Issues regard Kitten HTML parser (inc. Markdown)
tests
Related to tests (unit, end-to-end, etc.) in some way
usability
Anything that affects usability
accessibility
Accessibility-related issues
API
Issues related to Kitten’s public interface (for authors)
bug
Something is not working
CLI
Command-line interface related issues (Kitten commands)
contribution welcome
Contributions are very welcome, get started here
crash
A bug that crashes the server
deployment
Related to the deployment of Kitten apps/sites
duplicate
This issue or pull request already exists
enhancement
New feature
feature branch
Relates to a feature being developed in a separate branch (not main)
good first issue
Interested in contributing? Get started here.
help wanted
Need some help
installer
Issues related to the installation script
invalid
Something is wrong
is this still an issue?
Issues that need to be reproduced to confirm they haven’t already been fixed or made redundant
linux
Linux-specific issue
localisation
Localisation and internationalisation-related issues
macOS
macOS-specific issue
question
More information is needed
security
Security related issues
suggestion
upstream
Related to an upstream repository, already reported there
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kitten/app#311
Reference in a new issue
kitten/app
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?