Skip to content
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

Questions & Feedback about h0 #3

Open
mmocny opened this issue Nov 25, 2022 · 7 comments
Open

Questions & Feedback about h0 #3

mmocny opened this issue Nov 25, 2022 · 7 comments

Comments

@mmocny
Copy link

mmocny commented Nov 25, 2022

Fun project! :D

I figured I would leave some comments after reading the README.md, going through some examples, and reading a bit of the source for the first time:

  • The concept of scope vs route() seems confusing to me.

    • It is my understanding that scope dictates which template file is used to serve a given request, and that seems like the primary job of a "router".
    • It is my understanding that route() maps a Request to a ViewModel, and that seems like the primary job of a "loader".
    • If that is correct, I think it would be clearer to rename these accordingly.
  • It looks to me like you will always hard navigate across different scope, and that re-rendering client-side only works if it shares a template?

    • Is this a fundamental opinion of h0 or just a detail of current implementation?
    • I think this is not the typical definition of SPA and could be clarified in the docs.
    • I don't think it's a bad idea to separate when to go back just for data vs when to go back for HTML... but I think the way it currently works risks bloating the initial client.js payload to include the whole application, effectively, to gain SPA behaviours.
  • Looking at some examples, it seems route() is always sent to client. It's up to the application to decide when to run on client or to go back to server...

    • this is a neat idea, but it may be useful to make this pattern cleaner for typical cases...
    • Is it expected that h0 should support non-SSR, where route() must be able to run on client? I feel this is already not true for some of the examples...
    • If not, consider making route() always "server-only", and then having a separate, optional function that can run on client as well, perhaps interceptRoute().
    • when implemented, interceptRoute() would either return its own Response or fallback to return route(request). On client, the default route() implementation would just be fetch.
  • I'm happy to see a simple mount() helper. I suspect that this is the only function that runs on client in the case of SSR? I.e. there is no need to re-render the initial UI on the client, right?

  • If run in "SPA mode", docs suggest that route() is called only once on the server and render() is called any number of times on the client -- but I think its always 1:1, right? Or is there something that could force a re-render on the client without a data fetch?

  • In a future update, would it be possible to run render() on server and return the HTML to the client, and apply a delta locally instead?

    • There are some existing novel techniques to do this...
  • Would you consider implementing client-routing with the new Navigation API, which has polyfills and offers some advantages? I don't know if there is a version that runs on server...


That's it for now :P

Generally, I like the simplicity & structure.

I'm not sure I'm quite with you RE: "declarative and reactive is bad", but I think it's nice to have framework that opts-out of that part. Would be interesting to experiment with layering view-only libraries into your render().

@noamr
Copy link
Owner

noamr commented Nov 26, 2022

Fun project! :D

Thanks, and thanks for all the detailed feedback! Detailed feedback is my favourite.

I figured I would leave some comments after reading the README.md, going through some examples, and reading a bit of the source for the first time:

  • The concept of scope vs route() seems confusing to me.

    • It is my understanding that scope dictates which template file is used to serve a given request, and that seems like the primary job of a "router".

Think of scope as the root route for the whole app, something like app.use(scope, router) in express. The HTML needs to know that, because of links and forms. Maybe the name can be different to make it clearer. Suggestions? Ah I see you have one below

  • It is my understanding that route() maps a Request to a ViewModel, and that seems like the primary job of a "loader".

It also does the same as an express router. A loader is usually about a specific file type, no? Perhaps route.rootPath?

  • If that is correct, I think it would be clearer to rename these accordingly.
  • It looks to me like you will always hard navigate across different scope, and that re-rendering client-side only works if it shares a template?

Yes. The idea is that you share a template for client-side rendering, and do the app-specific thing inside route.

  • Is this a fundamental opinion of h0 or just a detail of current implementation?

It's a fundamental starting point. The idea is in the name - h0 (html first). So your HTML is the first source of truth for your entire app. Also implementation-wise, render is what "changes" your view, and it relies on having one document rather than diffing separate HTML files.

  • I think this is not the typical definition of SPA and could be clarified in the docs.

I don't follow. How I understand "SPA" is that forms and links within your app don't mandate a refresh. It's perhaps not the typical nextjs-like implementation of SPA. Is that what you mean?

  • I don't think it's a bad idea to separate when to go back just for data vs when to go back for HTML... but I think the way it currently works risks bloating the initial client.js payload to include the whole application, effectively, to gain SPA behaviours.

You don't "go back for HTML". Your client.js should be pretty small exactly because it doesn't include HTML - it includes only changes - how dynamic content is injected into the DOM. I can see a future though where part of your client.js performs dynamic JS imports if it becomes heavier.

  • Looking at some examples, it seems route() is always sent to client. It's up to the application to decide when to run on client or to go back to server...

    • this is a neat idea, but it may be useful to make this pattern cleaner for typical cases...

Agreed, didn't get to it yet.

  • Is it expected that h0 should support non-SSR, where route() must be able to run on client? I feel this is already not true for some of the examples...

Yes, the TODOs example only works with client-side routing because the "backend" is localStorage.

  • If not, consider making route() always "server-only", and then having a separate, optional function that can run on client as well, perhaps interceptRoute().

Right now you do that by adding if (runtime === "window") in route, or when choosing whether to export route which is perhaps a bit clunky, but I wanted to keep the API surface succinct. I envision adding a more syntactic-sugary version of this later on.

  • when implemented, interceptRoute() would either return its own Response or fallback to return route(request). On client, the default route() implementation would just be fetch.

Yea that would be a good addition, I wanted to run a bit with the "basic" which uses the RUNTIME constant for this and then see if the API surface can be improved.

  • I'm happy to see a simple mount() helper. I suspect that this is the only function that runs on client in the case of SSR? I.e. there is no need to re-render the initial UI on the client, right?

Correct. There is no "framework state" (well, apart from a few attributes that help with list reconciliation) so there is no "hydration" except for adding event listeners etc.

  • If run in "SPA mode", docs suggest that route() is called only once on the server and render() is called any number of times on the client -- but I think its always 1:1, right? Or is there something that could force a re-render on the client without a data fetch?

Sounds like a bug in the docs. route() would run multiple times on the server. It runs once only in MPA mode.

  • In a future update, would it be possible to run render() on server and return the HTML to the client, and apply a delta locally instead?

I thought about this originally, let me explain why I went without it. "Applying the delta locally" means appRoot.innerHTML = htmlResultFromServer. This would reset internal element state - reset input, reload iframes, reset custom-element states, clear lists and recreate all their items, rewind video positions etc. You might have everything working fine with this model, and then you add an iframe and everything breaks... So to avoid this issue, render in SPA mode is a client-side function.

  • There are some existing novel techniques to do this...

Or do you mean something like existing VDOM/htmldiff libraries? I thought that running render on the client would be much simpler but I can see how a later version of this can support this kind of thing.

  • Would you consider implementing client-routing with the new Navigation API, which has polyfills and offers some advantages? I don't know if there is a version that runs on server...

I'd love to use the navigation APIs and considered it, but the polyfills don't support the navigate event which is the main thing I need from it :( So I would have to write my own polyfill that kind of looks like the event interception in client.ts. Keeping it open for when the API matures and is supported in other browsers.

That's it for now :P

Generally, I like the simplicity & structure.

Yay, thanks!

I'm not sure I'm quite with you RE: "declarative and reactive is bad", but I think it's nice to have framework that opts-out of that part. Would be interesting to experiment with layering view-only libraries into your render().

Hmm did I say they were bad? I said they were "magic" and I wanted to offer a tradeoff that opts out of magic.

@mmocny
Copy link
Author

mmocny commented Nov 28, 2022

something like app.use(scope, router) in express
It also does the same as an express router.

Ah, thats funny. I was using the terms in the way js frontend naming use them, but it seems backends/express overload these as well. I don't have enough experience with express to grok the naming conventions.

A loader is usually about a specific file type, no?

I was using loader() in the Remix sense, since h0 reminds me of that structure. (the other big one is action() for data writing).

In remix, loader() is generates the data (ViewModel) for a specific route, and it is used in render() function(s) of all nested components.
In remix, action() is what runs when you POST a new form update. I do see something about form handling in h0 but I haven't quite unpacked it yet.

I don't follow. How I understand "SPA" is that forms and links within your app don't mandate a refresh.

This we agree on, but...

With h0 it seems you have 2 options:

  • Define many narrow HTML template with many tight scopes. Each scope supports SPA-like updates, but requires MPA navs between scopes. Or,
  • A single giant HTML template for the whole site. This would be SPA for everything, but due to the way that route() and render() are sent to client, it seems akin to downloading the whole app. If you did this, you would only have one route() and render() entrypoint for the whole app for any URL, and would have to handle all the routing/rendering in a giant switch(). This seems to defeats the whole point of a router component.

This is why I say that h0 does not seem like an SPA to me, even though it has some ability for client-side interactive updates.

I think that it could easily change, tho, if you just supported SPA transitions between scopes, treated scopes as routes, change route() as a loader(), and leave render() as it is.

Another concept that may help is the idea of "(nested) layouts". Some "routes" share most of the UI (layout) even though the data source differs slightly (i.e. Movies App UI uses the same layout for different filters). Maybe scope makes sense based on having a common layout?

Just my 2cents!

Or do you mean something like existing VDOM/htmldiff libraries?

Not vdom, just dom diff (like htmx or hotwire/turbo). It lets you transition between MPA pages with SPA behaviours, without having to manage the whole site as a giant component tree. I think it would be a good potential fit for transitioning between scopes.

On the one hand, its just an optimization of MPA transitions -- but perhaps it would enable you to split your HTML templates into more fine grained routes...

@noamr
Copy link
Owner

noamr commented Nov 28, 2022

something like app.use(scope, router) in express

It also does the same as an express router.

Ah, thats funny. I was using the terms in the way js frontend naming use them, but it seems backends/express overload these as well. I don't have enough experience with express to grok the naming conventions.

A loader is usually about a specific file type, no?

I was using loader() in the Remix sense, since h0 reminds me of that structure. (the other big one is action() for data writing).

In remix, loader() is generates the data (ViewModel) for a specific route, and it is used in render() function(s) of all nested components.

In remix, action() is what runs when you POST a new form update. I do see something about form handling in h0 but I haven't quite unpacked it yet.

I don't follow. How I understand "SPA" is that forms and links within your app don't mandate a refresh.

This we agree on, but...

With h0 it seems you have 2 options:

  • Define many narrow HTML template with many tight scopes. Each scope supports SPA-like updates, but requires MPA navs between scopes. Or,

  • A single giant HTML template for the whole site. This would be SPA for everything, but due to the way that route() and render() are sent to client, it seems akin to downloading the whole app. If you did this, you would only have one route() and render() entrypoint for the whole app for any URL, and would have to handle all the routing/rendering in a giant switch(). This seems to defeats the whole point of a router component.

This is why I say that h0 does not seem like an SPA to me, even though it has some ability for client-side interactive updates.

I think that it could easily change, tho, if you just supported SPA transitions between scopes, treated scopes as routes, change route() as a loader(), and leave render() as it is.

Another concept that may help is the idea of "(nested) layouts". Some "routes" share most of the UI (layout) even though the data source differs slightly (i.e. Movies App UI uses the same layout for different filters). Maybe scope makes sense based on having a common layout?

Just my 2cents!

Or do you mean something like existing VDOM/htmldiff libraries?

Not vdom, just dom diff (like htmx or hotwire/turbo). It lets you transition between MPA pages with SPA behaviours, without having to manage the whole site as a giant component tree. I think it would be a good potential fit for transitioning between scopes.

On the one hand, its just an optimization of MPA transitions -- but perhaps it would enable you to split your HTML templates into more fine grained routes...

Ah I see the misunderstanding. route() is not always sent to the client. The author decides that based on the RUNTIME global, letting the minifier fold that code. That's the concept at least, it's not well documented yet. What's always sent to the client is the render function.

@noamr
Copy link
Owner

noamr commented Nov 29, 2022

something like app.use(scope, router) in express
It also does the same as an express router.

Ah, thats funny. I was using the terms in the way js frontend naming use them, but it seems backends/express overload these as well. I don't have enough experience with express to grok the naming conventions.

A loader is usually about a specific file type, no?

I was using loader() in the Remix sense, since h0 reminds me of that structure. (the other big one is action() for data writing).

In remix, loader() is generates the data (ViewModel) for a specific route, and it is used in render() function(s) of all nested components. In remix, action() is what runs when you POST a new form update. I do see something about form handling in h0 but I haven't quite unpacked it yet.

OK, I think fetchModel and renderView would make things easier to understand.

I don't follow. How I understand "SPA" is that forms and links within your app don't mandate a refresh.

This we agree on, but...

With h0 it seems you have 2 options:

  • Define many narrow HTML template with many tight scopes. Each scope supports SPA-like updates, but requires MPA navs between scopes. Or,
  • A single giant HTML template for the whole site. This would be SPA for everything, but due to the way that route() and render() are sent to client, it seems akin to downloading the whole app. If you did this, you would only have one route() and render() entrypoint for the whole app for any URL, and would have to handle all the routing/rendering in a giant switch(). This seems to defeats the whole point of a router component.

So the idea is that route() in most cases is on the server, and render() can have dynamic imports if you so choose (but it's not part of the framework).
I think it would be better to change the API a bit so that it's clear that you don't have to send route() to the client.

@noamr
Copy link
Owner

noamr commented Nov 29, 2022

OK made some changes to reflect these comments:

  • route() renamed to fetchModel()
  • rener() renamed to renderView()
  • Instead of relying on the RUNTIME constant, fetchModel now has an optional runtime boolean (client-only / server-only / nothing).

@noamr
Copy link
Owner

noamr commented Nov 29, 2022

With h0 it seems you have 2 options:

  • Define many narrow HTML template with many tight scopes. Each scope supports SPA-like updates, but requires MPA navs between scopes. Or,
  • A single giant HTML template for the whole site. This would be SPA for everything

You touched on the exact point of h0. The idea is that if the app is simple enough to be sent to the client as a single HTML, then it's simple enough to be authored as a "master" HTML template (with includes, modules, web components etc to make it easier). If it gets more complicated, maybe it's time to MPA it?

@mmocny
Copy link
Author

mmocny commented Nov 29, 2022

Huzzah! Thanks for the quick answers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants