sveltejs / kit

web development, streamlined
https://kit.svelte.dev
MIT License
18.31k stars 1.87k forks source link

Server Load function for components #10632

Open TomDo1234 opened 11 months ago

TomDo1234 commented 11 months ago

Describe the problem

Basically, I think its a good idea to have server logic for components for Sveltekit.

Yes, I understand that we have streaming which gives Sveltekit performance equal to RSC. However its not always about the performance.

Imagine a component which inherently makes the same database query, such as a component displaying the amount of visits the site has all the amount of cars that are in the database. I don't want to have to put that data and query on the page.server.ts every single page that has this component. I want the server logic to be part of the component itself. That keeps things encapsulated and free from me having to jam the query into the page server ts , then pass things in as props or god forbid a store. This is especially useful if for example your "Amount of Cars" component runs 5 levels down the component tree from the +page.svelte. Allowing server logic on components leads to more freedom and can allow for more robust programs that are easier to read and reason about.

I think it will lead to more options for the developer to design their application, and can lead to better designed and more modulated components and application logic. Potentially, you can even have caching as well, in order to load the component faster if it is rendered multiple times in the app.

If you want to go super crazy, you can even make it so that these "Server Components" (Or should I say, components with coupled server logic) can take props (Though these props are static and are decided by the developer at build time, so for example you can only put in constant expressions) and use those props for both their client logic and server logic.

A super duper crazy idea would be to not constrain the props to constant expressions, so they can be dynamic during runtime, and every time a prop changes Svelte detects a change in the prop the same way it detects changes in a reactive expression's dependency, and then rerun server logic.

All in all, regardless of whether you agree with the crazy ideas of the 2 preceding paragraphs, I believe that at the very least... Giving components its own server logic is the future. It is part of the reason I have been going back to Next 13 (App directory) at work. Fine, you can complain about "use client" all you want, but I am not advocating for full blown server components that do not have access to client side JS like how RSCs cannot have useState in them. Instead I am proposing allowing server logic, essentially a +page.server.ts from within a component itself. The component can still have its client side stuff like all variables being reactive, reactive expressions, onMount etc. Except there is a section inside the component where you can define a load function which runs on the server, before svelte serves the HTML and JS,CSS to the client. This way, nobody is gonna get confused like with React people having to remember to put "use client" on things. This server logic is just going to be an optional add on if the developer wants to design a component that has inherent server logic. If people want to stick to the good old page load methodology, they can stick to that.

Describe the proposed solution

There should be a separate script tag in the component to describe the server load function, at the most basic level this could just be like the simple page server load function

event is just regular RequestEvent, props is the component props, you can if you want put props as part of event but whatever.

Alternatives considered

Allow top level await on a component, this way you can run an async process or logic and put that as a variable the component can use without using onMount, onMount is client side only which is why it might not always be useful.

However, this comes with the downside that I believe secrets cannot be used... Because even if it runs server side the secret is still exposed.

Importance

i cannot use SvelteKit without it

Additional Information

No response

elliott-with-the-longest-name-on-github commented 11 months ago

On the other side, I'm strongly against having fetching logic associated with components, as it's way too easy to waterfall yourself to oblivion. The only way this works is with aggressive caching and request deduplication, which has to work "magically", and never does in reality. This feels great in Next's app directory, until you've written a medium-to-large-sized application and are trying to debug why a given request isn't being properly cached and is causing 1s+ render time due to network waterfalls.

I'm very strongly of the opinion that network boundaries and rendering should have a healthy and strong delineation, and the fact that SK enforced this makes my applications much easier to reason about and reread later.

TomDo1234 commented 11 months ago

On the other side, I'm strongly against having fetching logic associated with components, as it's way too easy to waterfall yourself to oblivion. The only way this works is with aggressive caching and request deduplication, which has to work "magically", and never does in reality. This feels great in Next's app directory, until you've written a medium-to-large-sized application and are trying to debug why a given request isn't being properly cached and is causing 1s+ render time due to network waterfalls.

I'm very strongly of the opinion that network boundaries and rendering should have a healthy and strong delineation, and the fact that SK enforced this makes my applications much easier to reason about and reread later.

I was thinking that since Sveltekit already has anti-waterfall promise unwrapping on the load function, it can be done on a "component tree level" here.

If a page has a server logic component, sveltekit will scan the page's load function and the server logic component, and prevent waterfall by unwrapping both load functions at the same time?

Conduitry commented 11 months ago

Regardless of whether this is a good idea from an app architectural perspective, there are several technical hurdles here.

Having the fetch inside the component itself (#10729) runs into JS language limitations that aren't going to be resolved any time soon, and certainly not in all the runtimes people will expect to be able to run their apps in on the server.

Having external load functions (that look like our existing load functions) avoids the language hurdle, but then runs into the synchronousness of SSR. With route-level load functions, we can know what we want to load (by matching the URL against our routes) without having to render any components. With a component-level load function, we'll have no idea which components we're going to want to render, and once we start rendering them, we need to finish that synchronously, and there's no opportunity to pause that and load the data. We're at least not running up into limitations of JS here, but this would still be a big change to Svelte as well, and I don't see this being valuable enough that we devote time to it in the foreseeable future.

liamdiprose commented 5 months ago

I've found a solution that might meet the OP's encapsulation requirement:

combining Vite's import.meta.glob with <script context="module">:

MyComponent.svelte

<script context="module">
 export async function load({ params }) {
     return {
         message: "Hello World"
     }
 }
</script>

<script>
 export let data
</script>

{#await data}
{:then data}
<div>
    Message: {data.message}
</div>
{/await}

+page.js

const component_modules = import.meta.glob("./*.svelte", { eager: true })

const loadable_components = Object.fromEntries(
  Object.entries(component_modules)
        .filter(([name, _module]) => !name.startsWith("./+"))
        .map(([component_filename, module]) => {
          const component_name = component_filename.replace(/\.[^/.]+$/, "").slice(2)
          return [ component_name, module ]
        })
        .filter(([_name, module]) => typeof module.load === "function")
)

export async function load(load_event) {
  const component_data = Object.fromEntries(
    Object.entries(loadable_components)
          .map(([name, module]) => [name, module.load(load_event)])
  )

  return {
    component_data
  }
}

+page.svelte

<script>
 import MyComponent from './MyComponent.svelte'

 export let data
</script>

<MyComponent data={data.component_data.MyComponent} />

Note:

sommerper commented 4 months ago

@TomDo1234 I think this is a great idea! Encapsulation makes for cleaner code and is easier to maintain.

Right now I use https://tanstack.com/query/ so I can ssr components but it still requires an api call on the +page.svelte and subsequently the same call on the component.svelte which is less than ideal because I have to maintain code related to the component in two places.

Having +page.svelte wait for components to render is a good idea and if for some reason this creates pitfalls then we'll just have to watch out for them while creating the component.

I'd rather have the option, even though it adds complexity, than not having it at all.