-
Notifications
You must be signed in to change notification settings - Fork 781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Re-purpose / reuse root for SSR hydration / prerendering. #319
Comments
By the way, with this now it's also possible to do what until now was impossible. Use the app({ root: document.body, view: () => <body> ... </body> }) |
I really liked @Swizz 's idea of providing emit to the load event, so by default, the app would return emit. I cant say for sure which is most rational/useful to pass emit("load", ...) - emit or root? But my hunch and personal opinion is that it should be emit. Esp if it's possible to solve SSR anyway, with a mixin. |
@zaceno The By the way, this has nothing to do with the return value of the app() call. We can continue to return |
I still think you would need node, not root passed to the load event @jbucaran . Worrying about the extra work of hydration seems extreme since it only ever runs once. But I am completely onboard with moving this out of core if that's what you want. |
@jbucaran Isn't the idea that app will return the result of the load-event? Or am I getting old and new app event names mixed up? |
@zaceno I should have made it clearer that we can't reuse So, hyperapp 0.11.0 will continue to return |
Oh, yes, I misunderstood - thanks for clarifying! |
I don't see why that would be needed, but can you provide an example?
Is it really extreme? Beforeapp({
view: () =>
<main>
<h1>My app.</h1>
</main>
}) Afterapp({
view: () =>
<main>
<h1>My app.</h1>
</main>,
mixins: [Hydrator],
root: document.querySelector("[data-hyperapp-ssr]"),
}) The new approach has more code and requires more user intervention, but it also makes it very obvious and at the same time offers more flexibility as it leaves it up to you to implement hydration. I like how it forces the user to target the root |
👍 I'm in favor of this idea. Replacing the root node makes more sense, and it's one less useless However you decide to move the hydration out of core, that's probably for the best also. No reason to ship code in core that 75% of your users won't be needing (number pull out of thin air). |
@SkaterDad It also lets you render a |
@jbucaran the node variable is required for hydration in general, but root is already declared by the user so can be sent to the mixin by the user rather trivially. Without node the result of your Hydrator.events.load() can't provide the app with the vnode skeleton. Maybe it is accessible via some js voodoo that I am missing but app is basically a closure and the node var is hidden from the mixin unless I am missing something. |
@andyrj Actually events can return values. I wrote this above, but here it is again for convenience: app({
events: {
load(state, actions, root) {
return myOwnHydrationFormula(root)
}
}
}) |
\cc everyone When I refer to |
I apologize for missing that, I see what you are going to do, you are right node is not necessary if you do that. Still getting my coffee this morning heh. |
@andyrj With this new information, how do you feel about the proposal? You are who originally brought SSR to HyperApp, so I value your opinion here! 🙏 EDIT: (Everyone's as well, but you know what I mean ^^') |
But I still think adding root to the load event is un-necessary. var root = document.querySelector("[data-hyperapp-ssr]")
app({
view: ...,
mixin: [Hydrator(root)],
root
}) Edit: just to clarify I am fine with either way I just don't see the need to add root to the args of load is all. |
@andyrj Yeah, sure, it's just for convenience. |
I do have one more concern with this proposal. If we overwrite the target node during init that will cause issues for my current webpack hmr setup. If I target a node with id="root" and our app obliterates it, it does make the final output "cleaner", but then in module.hot I have no element to target for the re-initialization of app(). 😞 This wouldn't be a problem if we didn't have the weird append default case as I could just use body without specifying a root... |
I am sure there must be a workaround, since overwriting the target node is how, e.g., React behaves. |
Can you share some code with the problem? I have no idea what you mean. |
I haven't messed with react official for some time, but last I checked it left the render target in tact, as do inferno and preact. I will use your proposed example above as a base. <body>
<div id="app"></div>
</body> /* app.js */
import { h, app } from "hyperapp";
export default (state, target) => {
app({
view: () =>
<main>
<h1>My app.</h1>
</main>,
root: target
})
}; /* index.js */
import app from "./app";
var target = document.getElementById('app');
app(state, target);
if (module.hot) {
module.hot.accept("./app", () => {
// blows up below because target no longer exists in the document if we obliterate it...
app(window.state, target)
})
} edit: corrected code example for basic webpack hmr to show where the problem will show up if render target is removed from dom |
@andyrj Are you sure you don't know how to fix this? 😏 |
I don't see a good work around, I would need to document.body.innerHTML='<div id="app"></div>' before the call to app() in module.hot, so back to the flickering lol. Don't change what you want to do based around my issue, I am just saying it will cause issues for my use case of wanting ssr with hmr without flickering. I can always keep my private fork going... I still despise having to add data attributes and not being able to SSR to body tag to keep the strange append use case so I will likely always be using my private fork. But I could see this being strange to others that also use webpack-hmr. |
@andyrj I can see two clean ways. Using rehydration!<body>
<div id="app">
<!-- rest of the tree (or just the top-level element a.k.a root) -->
</div>
</body> export default (state, root) =>
app({
view: () =>
<div id="app">
<h1>My app.</h1>
</div>,
root
}) If your hydrator is doing its job correctly, then HyperApp's patch() should not end up replacing the element, but reusing the root (and as many children as provided). Simulating the previous behavior (no-hydration)<body>
<main> <!-- safe -->
<div id="app"></div> <!-- will be replaced -->
</main>
</body> const root = document.getElementById("app")
// ... A third workaround is to call getElementById("app") twice, but I don't see why you'd prefer that. Let me know if this makes sense. |
LGTM! Looks great on my projects this will let me get rid of my fork I have been using. 💯 👍 edit: talking about #317 of course... |
Implemented in #317. |
Our current hydration implementation is broken! We are simply avoiding the duplication of your SSR nodes, but we are still patching everything from scratch. 😆
We could improve it this way:
Unfortunately, this still does not take into account empty text nodes, which if not trimmed, would yield a tree that is not consistent with the new tree returned by the view, causing patch to do more work than it needs if we were hydrating correctly.
A more complex solution could be explored, but then how much is enough and can this be moved outside core?
I've been thinking about this today and considered many alternatives, and here is my proposal.
Re-purpose
root
for SSR-use for what-would-be the old element during the initial render.I originally chose the name
root
because it was short and simple, but now I think thatroot
should describe your application's top element:<main>
,<div>
, etc., and NOT the HTML element to act as a container / host of your application tree (which is what we do now).How will this affect existing apps?
Apps not using
root
.Everything will continue to work as usual. 🎉
Apps using
root
.Say you have this HTML:
and used
root
like this:Now your app will render overwriting / obliterating
<div id="app">
, so the final HTML will look like this:This is similar to how React behaves.
SSR
Using
root
for SSR pages would allow us to remove this part from core.And use instead:
Or a different data attribute; it's up to you.
Finally, the old node for the initial render. Instead of introducing a new property, I propose using the
load
event, currently unused.so we can assign that result value to the node inside core like this:
Using a mixin for that would work as well. Obviously, we'd call that mixin
Hydrator
!SSR Example
A full SSR page example would work like this:
/cc @andyrj @ngryman @SkaterDad @lukejacksonn @zaceno
The text was updated successfully, but these errors were encountered: