Fix hydration with empty spread attributes (#4647)

* Fix hydration of empty spread attributes

* add a regression test

* fix doctest
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-10-03 19:41:23 -07:00
1 parent f48aec7eff
commit 03801e1312
8 files changed
+94 -2

No files matched your search

Generated
+7
View File
@@ -5926,6 +5926,13 @@ dependencies = [
"tokio",
]
[[package]]
name = "dioxus-playwright-fullstack-spread-test"
version = "0.1.0"
dependencies = [
"dioxus",
]
[[package]]
name = "dioxus-playwright-fullstack-test"
version = "0.1.0"
+1
View File
@@ -130,6 +130,7 @@ members = [
"packages/playwright-tests/fullstack",
"packages/playwright-tests/fullstack-errors",
"packages/playwright-tests/fullstack-mounted",
"packages/playwright-tests/fullstack-spread",
"packages/playwright-tests/fullstack-routing",
"packages/playwright-tests/fullstack-hydration-order",
"packages/playwright-tests/suspense-carousel",
+5 -1
View File
@@ -819,10 +819,14 @@ impl VNode {
}
};
// Write the value for each attribute in the group
for attr in &**attribute {
self.write_attribute(attribute_path, attr, id, mount, dom, to);
dom.set_mounted_dyn_attr(mount, attribute_idx, id);
}
// Set the mounted dynamic attribute once. This must be set even if no actual
// attributes are present so it is present for renderers like fullstack to look
// up the position where attributes may be inserted in the future
dom.set_mounted_dyn_attr(mount, attribute_idx, id);
}
}
@@ -0,0 +1,13 @@
// @ts-check
const { test, expect } = require("@playwright/test");
test("spread attributes hydrate", async ({ page }) => {
await page.goto("http://localhost:7979");
// Expect the page to contain the button
const counter = page.locator("#counter");
await expect(counter).toHaveText("Count: 0");
// Clicking on the button should increment the count
await counter.click();
await expect(counter).toHaveText("Count: 1");
});
@@ -0,0 +1,15 @@
[package]
name = "dioxus-playwright-fullstack-spread-test"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { workspace = true, features = ["fullstack"] }
[features]
default = []
server = ["dioxus/server"]
web = ["dioxus/web"]
@@ -0,0 +1,40 @@
//! Regression test for <https://github.com/DioxusLabs/dioxus/issues/4646>
use dioxus::prelude::*;
fn main() {
dioxus::launch(|| {
rsx! {
Comp {}
Comp {}
Button {}
}
});
}
#[component]
fn Button() -> Element {
let mut count = use_signal(|| 0);
rsx! {
button {
id: "counter",
onclick: move |_| {
count += 1;
},
"Count: {count}"
}
}
}
#[component]
fn Comp(#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>) -> Element {
rsx! {
div {
width: 100,
div {
..attributes,
}
}
}
}
@@ -108,6 +108,15 @@ module.exports = defineConfig({
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-spread"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 7979',
port: 7979,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-routing"),
command:
+4 -1
View File
@@ -301,9 +301,12 @@ impl WebsysDom {
let id = vnode
.mounted_dynamic_attribute(*id, dom)
.ok_or(VNodeNotInitialized)?;
// We always need to hydrate the node even if the attributes are empty so we have
// a mount for the node later. This could be spread attributes that are currently empty,
// but will be filled later
mounted_id = Some(id);
for attribute in attributes {
let value = &attribute.value;
mounted_id = Some(id);
if let AttributeValue::Listener(_) = value {
if attribute.name == "onmounted" {
to_mount.push(id);