sjc5 / hwy

Hwy is a fullstack web framework for driving a (p)react frontend with a Go backend. Includes end-to-end typesafety, file-based nested UI routing, and much more.
BSD 3-Clause "New" or "Revised" License
317 stars 3 forks source link

enhancement: `head` function within page route files could return JSX head elements (e.g. meta, title and link tags) instead of an array of objects. #48

Closed jaymanmdev closed 10 months ago

jaymanmdev commented 10 months ago

This is a subjective opinion, but if it is possible (I'm not sure what the internal reason for the current API is), then I think that it would be more ergonomic and intuitive to use a similar return as the usual page or component.

e.g. instead of:

export const head = () => {
  return [
    {
      title: "Hello world!"
    }
  ];
}

we could use:

export const head = () => {
  return <title>Hello world!</title>
}

or instead of:

export const head = () => {
  return [
    {
      tag: "link",
      rel: "stylesheet",
      href: "/fonts/..."
    },
   {
      tag: "meta",
      name: "description",
      content: "This is an application!"
    }
  ];
}

we could use:

export const head = () => {
  return <>
    <link rel="stylesheet" href="/fonts/..." />
    <meta name="description" content="This is an application!" />
  </>
}

Let me know what you think @sjc5! :)

sjc5 commented 10 months ago

So the reasoning IIRC is because the various nested routes need to "battle" to see which headers will win. Sometimes you'll have duplicates, and the children routes need to take priority in those situations. That's really trivial to do with an array-of-objects pattern (what Remix does as well) and becomes quite a hassle if you need to do it with JSX. Whereas right now, Hwy doesn't do any JSX parsing at all (that's all handled by Hono, or whatever other JSX renderer you may choose such as preact-render-to-string), to do this Hwy would have to start concerning itself with "understanding" JSX, if that makes sense.

jaymanmdev commented 10 months ago

So the reasoning IIRC is because the various nested routes need to "battle" to see which headers will win. Sometimes you'll have duplicates, and the children routes need to take priority in those situations. That's really trivial to do with an array-of-objects pattern (what Remix does as well) and becomes quite a hassle if you need to do it with JSX. Whereas right now, Hwy doesn't do any JSX parsing at all (that's all handled by Hono, or whatever other JSX renderer you may choose such as preact-render-to-string), to do this Hwy would have to start concerning itself with "understanding" JSX, if that makes sense.

Ahhh, that's fair enough - it has internal reasons similar to Remix. It makes sense when you think about having to override head tags in nested routes, similar to merging loader data between nested routes in Remix (not sure if that is currently a feature in Hwy, however it would be a cool addition for sure). In that case, it's fine the way it is. I just wasn't sure if it had reasons for it's design internally. JSX is easier to read and write from an ergonomic point of view, however, it is much more difficult to parse and understand behind the scenes. Shall I close this issue then?

sjc5 commented 10 months ago

Yeah we can close this one -- while I totally agree from a dev perspective, it's not worth the squeeze. And another counter-argument for it is that using standard arrays/objects is a bit easier to do programmatically if you want to as well.

Can you point me to the Remix docs on loader merging @jaymanmdev? I'm not sure what you mean by that. Maybe it's a newer feature.

jaymanmdev commented 10 months ago

Yeah we can close this one -- while I totally agree from a dev perspective, it's not worth the squeeze. And another counter-argument for it is that using standard arrays/objects is a bit easier to do programmatically if you want to as well.

Can you point me to the Remix docs on loader merging @jaymanmdev? I'm not sure what you mean by that. Maybe it's a newer feature.

I agree 100%, you can do much more programmatically and there is more flexibility in using object-related data structures for this type of thing. For example, you can map in an array of links based on server/internal or database data received.

I might be wrong, but I thought that Remix attempts to merge loader data together into a single object to be retrieved by the 'useLoader' hook. Granted, I might be entirely wrong here as I haven't used Remix all that much. I know that they do the same thing as Hwy in that they run nested loaders in parallel to avoid waterfalls and things of that nature, but I couldn't find any docs regarding merging loader data so I might be wrong haha Also, since Hwy supports and builds upon Hono's experimental HTML streaming support (server components), which are so freakin' cool and have played a huge part in the development of my Pokémon app, what is the usefulness of loaders? Is it more as an API route interface (same as actions for POST requests and form submissions)? and an alternative way to load data? I know that it is heavily inspired by Remix's patterns. I know that Remix is going to support React Server Components quite soon. I feel like in a lot of cases, loaders wouldn't be the most efficient way to load data, as it blocks the entire page from showing any content at all, which is fixed with streaming.

sjc5 commented 10 months ago

Loaders will almost always be the most efficient way to load data because it will parallelize all the nested routes loaders.

When you fetch inside of the components, you'll create waterfalls for anything that's nested.

My opinion is that your goal should be for your underlying data fetching to be fast enough that your first page load doesn't feel slow without an intermediate loading state, but you can always also manually parallelize in your parent component with Suspense and then either prop drill or add the results to the Hono context to get parallelized Suspense on that initial page load if that's your goal. IMO usually not the right choice or worth it, but definitely an option.

jaymanmdev commented 10 months ago

Loaders will almost always be the most efficient way to load data because it will parallelize all the nested routes loaders.

When you fetch inside of the components, you'll create waterfalls for anything that's nested.

My opinion is that your goal should be for your underlying data fetching to be fast enough that your first page load doesn't feel slow without an intermediate loading state, but you can always also manually parallelize in your parent component with Suspense and then either prop drill or add the results to the Hono context to get parallelized Suspense on that initial page load if that's your goal. IMO usually not the right choice or worth it, but definitely an option.

Yeah, I guess it is really up to personal preference. I like being able to fetch inside of components and choosing where to break up my boundaries. Yes, it can cause layout shift issues with waterfalls, but if you design things correctly, then I think that there are benefits to such a design. There are auto-parallelisation benefits to using loaders too as you said, so I guess it is a per use case-basis type of thing. :) For example, in my Pokémon app, I was initially fetching the Pokémon data inside of the loader. This would cause the data to take up to a few seconds to load, and if there were fetch errors (which happen occasionally as you know), then it would take even longer. With Suspense I was able to speed up the load time of the header and footer to half a second or so, and then use a loading spinner component to show that more data is loading until successful Pokémon data comes in - I feel like this is much better UX, but that is all up to personal interpretation too.

sjc5 commented 10 months ago

Yeah several seconds is quite bad, and I agree you need some sort of loading state in that situation.