-
Notifications
You must be signed in to change notification settings - Fork 79
Description
Intro
elm/virtual-dom has a _VirtualDom_virtualize
function, that looks at the existing HTML (DOM nodes) and turns it into virtual nodes at initialization.
#187 makes the implementation of _VirtualDom_virtualize
complete, so that Elm doesn’t need to do much to the existing DOM nodes in a server-rendered application – it basically leaves them be and just attaches event listeners here and there.
However, there is one thing that isn’t handled: Taking care of user input that happened before Elm initialized on the client.
Example
Imagine you have built an e-commerce site with elm-pages. You send an email to a customer: “Please leave a review for the Space Socks you bought last week!” There’s a direct link to the product page. At the bottom, there’s a <textarea>
for writing a review.
The customer clicks the link. Since the page is built with elm-pages, they get server-side rendered HTML. The customer is on a train, and they lose the Internet connection for a while, so only the HTML and CSS loads, but the Elm JS is delayed. They start writing their review. What should happen when the Elm JS loads?
Elm initializes, and in init
the model is set to have an empty string as default value for the review text. On the first render, Elm will then clear the textarea (to match the model). The customer loses all text they wrote.
Potential solution
One thing we could do, is to keep track of that “this is the first render after virtualization. For form control elements, don’t update .value
, instead trigger the input
event so that Elm gets to know about the state of the element (assuming the Elm code listens for the input
event)”.
What do other frameworks do?
Interestingly, both React and Preact seem to not set value
after hydration, but won’t let the app know about the actual values in the form control, leaving the app out of sync until more events happen.
Here’s the React code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>React Hydration Test</title>
</head>
<body>
<!-- This is server-rendered HTML: -->
<div id="root">
<div>
<h1>Preact</h1>
<p>Textarea:</p>
<textarea></textarea>
<p>Input:</p>
<input />
<p>Checkbox: false</p>
<input type="checkbox" value="false" />
</div>
</div>
<script type="module">
import { hydrateRoot } from "https://esm.sh/react-dom@18/client?dev";
import {
createElement as h,
useState,
} from "https://esm.sh/react@18?dev";
function App() {
const [textarea, setTextarea] = useState("");
const [input, setInput] = useState("");
const [checkbox, setCheckbox] = useState(false);
return h(
"div",
null,
h("h1", null, "React"),
h("p", null, "Textarea: " + textarea),
h("textarea", {
value: textarea,
onInput: (e) => setTextarea(e.target.value),
}),
h("p", null, "Input: " + input),
h("input", {
value: input,
onInput: (e) => setInput(e.target.value),
}),
h("p", null, "Checkbox: " + checkbox),
h("input", {
type: "checkbox",
value: checkbox,
onInput: (e) => setCheckbox(e.target.checked),
})
);
}
// Simulate a slow network.
setTimeout(() => {
hydrateRoot(document.getElementById("root"), h(App));
console.log("Hydrated!");
}, 5000);
</script>
</body>
</html>
And the Preact code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Preact Hydration Test</title>
</head>
<body>
<!-- This is server-rendered HTML: -->
<div id="root">
<div>
<h1>Preact</h1>
<p>Textarea:</p>
<textarea></textarea>
<p>Input:</p>
<input />
<p>Checkbox: false</p>
<input type="checkbox" value="false" />
</div>
</div>
<script type="module">
import { h, hydrate } from "https://esm.sh/preact@10.19.5?dev";
import { useState } from "https://esm.sh/preact@10.19.5/hooks?dev";
function App() {
const [textarea, setTextarea] = useState("");
const [input, setInput] = useState("");
const [checkbox, setCheckbox] = useState(false);
return h(
"div",
null,
h("h1", null, "Preact"),
h("p", null, "Textarea: " + textarea),
h("textarea", {
value: textarea,
onInput: (e) => setTextarea(e.target.value),
}),
h("p", null, "Input: " + input),
h("input", {
value: input,
onInput: (e) => setInput(e.target.value),
}),
h("p", null, "Checkbox: " + checkbox),
h("input", {
type: "checkbox",
value: checkbox,
onInput: (e) => setCheckbox(e.target.checked),
})
);
}
// Simulate a slow network.
setTimeout(() => {
hydrate(h(App), document.getElementById("root"));
console.log("Hydrated!");
}, 5000);
</script>
</body>
</html>
And some links:
- https://stackoverflow.com/questions/73567117/how-to-deal-with-text-input-changes-in-ssr-text-box-before-hydration-is-complete
- https://stackoverflow.com/questions/31227517/how-to-handle-early-input-to-isomorphically-rendered-forms
- Apps that mount over isomorphically rendered forms aren't aware of early input facebook/react#4293
- Fire change events for changes made to inputs before hydration facebook/react#12955
- Bug: form data is lost upon hydration facebook/react#26974