There's a few approaches.
We can take the React / Vue / et al approach:
import { collectResult } from "@lit-labs/ssr/lib/render-result.js"
import { render } from "lit"
import App from "./app.js"
app.get("/", async (req, res, next) => {
const app = await collectResult(render(new App()))
res.send(`
<!DOCTYPE html>
<html>
<head></head>
<body>
<div id="app">${app}</div>
</body>
</html>
`)
})import App from "./app.js"
import {render} from "lit"
render(new App(), document.querySelector("#app"))This approach is pretty tried and true, but does require both server + client are loading the exact same tree. If you plan to "sprinkle" pieces of Lit across a traditional "server rendered" app, this approach isn't as nice as it fully "takes over" rendering and everything must now be done with Lit.
- Very easy to implement
- Tried and true
- Requires to "own" the entire tree its rendering so you can't selectively add bits, unless you add specific "mounts"
What if instead, we wanted to "sprinkle" pieces of Lit, almost like an "island" architecture?
import { collectResult } from "@lit-labs/ssr/lib/render-result.js"
import { render } from "lit"
import Foo from "./foo.js"
import Bar from "./bar.js"
app.get("/", async (req, res, next) => {
const foo = await collectResult(render(new App()))
const bar = await collectResult(render(new App()))
res.send(`
<!DOCTYPE html>
<html>
<head></head>
<body>
<div data-component="foo">
${foo}
</div>
<div data-component="bar">
${bar}
</div>
</body>
</html>
`)
})import Watcher from "watcher"
import Foo from "./foo.js"
import Bar from "./bar.js"
const watcher = new Watcher()
.register("foo", Foo)
.register("bar", Bar)
// Pretend we do this in a MutationObserver and check if we've already rendered the component.
document.querySelectorAll("[data-component]").forEach((el) => {
const componentName = el.dataset.component
if (watcher.has(component)) {
hydrate(watcher.get(component), el)
}
})- Lets you selectively use Lit and not need to rewrite your whole rendering process. This is essentially like an "islands" architecture
- A component cannot affect its "host" container
- Things are a little "decoupled" when used this way.
What if you could insert a Lit hydrated element anywhere
import { collectResult } from "@lit-labs/ssr/lib/render-result.js"
import { render } from "lit"
import App from "./app.js"
app.get("/", async (req, res, next) => {
const html = await collectResult(render(new Layout()))
res.send(html)
})And imagine Layout has pieces rendered in the <head> / <body> and can either have static html bits, or nested components.
On the client, the way I expect this would work, is each "Component" is assigned a unique identifier and a comment node is injected above it. We would essentially "walk a tree" and hydrate + render every known identifier + comment node, and dump a big ole JSON payload for each unique identifier + component. like this:
{
"Layout-134422234": {},
"App-145242": {},
"Counter-134282": {state: {count: 50}}
"Counter-928492": {state: {count: 100}}
}and this is stored on a Map() that when a TreeWalker comes across the node, can provide the expected state.
The challenge with this is obviously nested trees and how does it work when an outer node re-renders...does it do granular updates of inner nodes? I don't know. What about data fetching in the component? How does that work?