Skip to content

Commit 71bf76f

Browse files
committed
Fix composable rendered twice in fullstack SSR projects
Generated fullstack client Main.kt called renderComposableRoot instead of hydrateComposableRoot, so the JS app appended new DOM below the server-rendered HTML instead of replacing it. - Switch generated fullstack client templates to hydrateComposableRoot - Clear container innerHTML in renderComposable even for SSR containers as a safety net against duplicates - Update test assertions to match new API call - Bump version to 0.7.0.2
1 parent 70f8c03 commit 71bf76f

5 files changed

Lines changed: 26 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.7.0.2] - 2026-02-10
6+
7+
### Fixed
8+
9+
- **Composable rendered twice in fullstack SSR projects** - The server-rendered HTML and the
10+
client-side app both appeared on screen because the generated client `Main.kt` used
11+
`renderComposableRoot` instead of `hydrateComposableRoot`, causing the JS app to append below
12+
the static SSR content rather than replacing it
13+
- **`renderComposable` safety net** - Calling `renderComposableRoot` on an SSR container now
14+
always clears existing content before rendering, preventing duplicate output even if
15+
`hydrateComposableRoot` is not used
16+
517
## [0.7.0.1] - 2026-02-09
618

719
### Fixed

summon-cli/src/jvmMain/kotlin/codes/yousef/summon/cli/generators/ProjectGenerator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,10 +1020,10 @@ fun main() {
10201020
return """
10211021
package ${variables["PACKAGE_NAME"]}
10221022
1023-
import codes.yousef.summon.renderComposableRoot
1023+
import codes.yousef.summon.hydrateComposableRoot
10241024
10251025
fun main() {
1026-
renderComposableRoot("${variables["ROOT_ELEMENT_ID"]}") {
1026+
hydrateComposableRoot("${variables["ROOT_ELEMENT_ID"]}") {
10271027
App()
10281028
}
10291029
}

summon-cli/src/jvmTest/kotlin/codes/yousef/summon/cli/generators/ProjectGeneratorTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ class ProjectGeneratorTest {
464464
val jsBootstrap = File(tempDir, "app/src/jsMain/kotlin/com/example/full/Main.kt")
465465
assertTrue(jsBootstrap.exists(), "JS bootstrap should exist inside the app module")
466466
val jsContent = jsBootstrap.readText()
467-
assertTrue(jsContent.contains("renderComposableRoot("), "JS bootstrap should use renderComposableRoot API")
467+
assertTrue(jsContent.contains("hydrateComposableRoot("), "JS bootstrap should use hydrateComposableRoot API")
468468

469469
val serverFile = File(tempDir, "backend/src/main/kotlin/com/example/full/Application.kt")
470470
assertTrue(serverFile.exists(), "Ktor server entrypoint should be generated in backend module")
@@ -534,8 +534,8 @@ class ProjectGeneratorTest {
534534
assertTrue(jsMain.exists(), "$type: JS Main.kt should exist")
535535
val jsContent = jsMain.readText()
536536
assertTrue(
537-
jsContent.contains("renderComposableRoot(\"$expectedRootId\")"),
538-
"$type: JS Main.kt should call renderComposableRoot(\"$expectedRootId\") but was:\n$jsContent"
537+
jsContent.contains("hydrateComposableRoot(\"$expectedRootId\")"),
538+
"$type: JS Main.kt should call hydrateComposableRoot(\"$expectedRootId\") but was:\n$jsContent"
539539
)
540540

541541
// Generated index.html root element should match

summon-core/src/jsMain/kotlin/codes/yousef/summon/Composable.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package codes.yousef.summon
22

33
import codes.yousef.summon.annotation.Composable
4-
import codes.yousef.summon.runtime.*
4+
import codes.yousef.summon.runtime.LocalPlatformRenderer
5+
import codes.yousef.summon.runtime.PlatformRenderer
6+
import codes.yousef.summon.runtime.RecomposerHolder
7+
import codes.yousef.summon.runtime.setPlatformRenderer
58
import org.w3c.dom.HTMLElement
69

710
/**
@@ -29,13 +32,12 @@ fun renderComposable(renderer: PlatformRenderer, composable: @Composable () -> U
2932
container.getAttribute("data-ssr") == "true"
3033

3134
if (isSSRContainer) {
32-
js("console.warn('renderComposable called on SSR container - this may break hydration. Use SummonHydrationClient instead.');")
33-
// Don't clear the container in SSR mode - it contains server-rendered content
34-
} else {
35-
// Clear container only for client-side rendering
36-
container.innerHTML = ""
35+
js("console.warn('renderComposable called on SSR container - use hydrateComposableRoot for hydration. Clearing SSR content to avoid duplicates.');")
3736
}
3837

38+
// Always clear the container to prevent duplicate rendering
39+
container.innerHTML = ""
40+
3941
try {
4042
setPlatformRenderer(renderer)
4143
val recomposer = RecomposerHolder.current()

version.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Centralized version information for Summon
22
# This file is the single source of truth for version information
33
# and is used by both the main project and example projects
4-
VERSION=0.7.0.1
4+
VERSION=0.7.0.2
55
GROUP=codes.yousef
66
ARTIFACT_ID=summon

0 commit comments

Comments
 (0)