Skip to content

fix: use push_renderer_if_inactive in compiled components#18058

Open
tomyan wants to merge 1 commit intosveltejs:svelte-custom-rendererfrom
tomyan:svelte-custom-renderer
Open

fix: use push_renderer_if_inactive in compiled components#18058
tomyan wants to merge 1 commit intosveltejs:svelte-custom-rendererfrom
tomyan:svelte-custom-renderer

Conversation

@tomyan
Copy link
Copy Markdown

@tomyan tomyan commented Apr 3, 2026

Hi @paoloricciuti — I'm excited about this PR. I've been trying out the custom renderer API for a terminal renderer and ran into an issue with reactive updates.

One problem I ran into is that when render() is called with a renderer that has been configured with context (e.g. a render queue for incremental updates), the compiled component's push_renderer($renderer) call overrides it with the statically imported default renderer. Effects created during the component capture the imported renderer, so when they re-run on $state changes, renderer.setText is called on the wrong instance — one without the mount-time configuration.

In practice this means that if you create a renderer with createRenderer() for the static import (as required by the compiler), and then create a second configured instance in your mount() function to pass to renderer.render(), the configured instance is only used during initial tree construction. Any subsequent reactive updates ($state changes triggering template_effect re-runs) go through the unconfigured import instead. This effectively makes reactive state changes invisible to the renderer's infrastructure — text updates, attribute changes, and DOM mutations from effects all bypass whatever context the render() caller set up.

The sequence:

  1. render() calls push_renderer(mountRenderer) — renderer with context ✓
  2. Compiled component calls push_renderer($renderer) — statically imported, no context ✗
  3. template_effect captures r: renderer — gets the import, not the mount renderer
  4. On $state change, effect re-run calls set_textrenderer.setText on wrong instance

To fix this I propose push_renderer_if_inactive() which only pushes if no renderer is currently active. The compiled component uses this instead of push_renderer, so if render() already pushed a renderer, the component respects it.

When render() already pushed a renderer, the compiled component should
not override it with its own imported renderer. This ensures that effects
created during mount capture the renderer provided by the render() caller,
not the statically imported one.

Without this fix, custom renderers that configure their renderer at
mount() time (e.g., with a RenderContext for incremental rendering)
have their configuration lost because the compiled component pushes
a different renderer instance.

Added push_renderer_if_inactive() which only pushes if no renderer
is currently active. Compiled components now use this instead of
push_renderer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 3, 2026

⚠️ No Changeset found

Latest commit: c47e06f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@svelte-docs-bot
Copy link
Copy Markdown

@paoloricciuti
Copy link
Copy Markdown
Member

Can you explain a bit more what you are trying to do? Why are you creating a separate renderer?

@tomyan
Copy link
Copy Markdown
Author

tomyan commented Apr 7, 2026

Thanks @paoloricciuti! I'm prototyping a terminal renderer using the custom renderer API - Svelte components rendered to a cell grid with ANSI escape sequences. It's been really great to work with.

I may be doing something the API doesn't intend — I'm creating the renderer at mount time with per-instance context (a render queue that tracks which nodes changed for incremental updates). The static import is a bare instance without this context. The issue is that effects capture the static import, so reactive updates bypass the mount-time renderer.

Is the expectation that there's only ever one renderer instance (the static import), and any state should be attached to that? I could make that work with a mutable global, but it would prevent mounting multiple independent component trees.

If per-mount context is a reasonable pattern, push_renderer_if_inactive would let the compiled component defer to whatever renderer render() set up. Happy to take a different approach if there's a better way.

Copy link
Copy Markdown

@codeCraft-Ritik codeCraft-Ritik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closure Capture Timing: The core issue is that template_effect (and other runes) capture the "current" renderer at the moment of initialization. If the compiled component pushes its own renderer onto the stack after the custom one, the effect is permanently tethered to the wrong instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants