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

Separate Load Functions for Server and Browser #10004

Closed
jdgamble555 opened this issue May 21, 2023 · 12 comments
Closed

Separate Load Functions for Server and Browser #10004

jdgamble555 opened this issue May 21, 2023 · 12 comments

Comments

@jdgamble555
Copy link

Describe the problem

Right now there is not a way to separate them. We can run the same code in browser-only, server-only, or both, but there is no way to run SEPARATE code in both server-only, browser-only versions.

Most database fetches cannot be run in the browser. This can be fixed by using just +page.server.ts, however, this forces a new database call from the server on each route reload. What if I want DIFFERENT code depending on if it is a server or browser call? This is currently impossible. You can't use the if (browser) option, as it will load the same code and packages for the server, which you don't need or can't use in the browser.

The only work-around is to completely move your server load function code outside of your load function, and create and endpoint. That way you can do: if (!browser), fetch data from the endpoint. This is extremely problematic, as you are forced to create an endpoint for something that is already loading on the server. This enables access to your data from an endpoint, which you don't want. Plus, with endpoints, you lose type-safety like you have with PageData or $page.

Describe the proposed solution

The most logical solution which would allow backwards compatibility is to allow BOTH the +page.server.ts file AND the +page.ts files. If it is the initial load, it will run +page.server.ts, and every call to the component afterwards will run the +page.ts file.

Alternatives considered

  • Another option maybe a +page.browser.ts file.
  • You could also add a export const separate = true
  • Allow two load functions in a +page.ts file (loadBrowser, loadServer)

Importance

would make my life easier

Additional Information

I can use SvelteKit without it, but I cannot use it correctly without an extraneous endpoint.

A good example of this is a feed. When I click on the item on the feed, I should not have to refetch that item if it is already in memory. There is no way to do this without an extraneous endpoint if I can only fetch the data on the server. With Firebase, for example, this could also potentially add extraneous read costs.

@elliott-with-the-longest-name-on-github
Copy link
Contributor

The most logical solution which would allow backwards compatibility is to allow BOTH the +page.server.ts file AND the +page.ts files. If it is the initial load, it will run +page.server.ts, and every call to the component afterwards will run the +page.ts file.

This would be breaking. You can already have both load functions; they just both run.

@david-plugge
Copy link
Contributor

They are both executed. You can use the event.data in your shared load function on the server and execute different code when running in the browser

haven't tested this, writing from my phone

export const load = (event) => {
  if (!browser) return event.data;

  return { custom: 1234 } 
} 

@jdgamble555
Copy link
Author

@tcc-sejohnson - Didn't realize that, good to know. However, this doesn't solve the problem of running them separately instead of consecutively.

@david-plugge - Not sure what you mean. I can check for the browser on the +page.ts, but browser will always return false on the server. If you can think of any work-arounds, I would love to test them!

Perhaps then the best feature request would be:

export const load_once = true for +page.server.ts. That way it will always only load once when the app is loaded...

J

@elliott-with-the-longest-name-on-github
Copy link
Contributor

I'm a bit confused about what you're trying to accomplish. There's a very high chance it's already possible. Can you describe the actual use case a bit more?

@jdgamble555
Copy link
Author

jdgamble555 commented May 21, 2023

@david-plugge - So it seems I can skip the load function on +page.server.ts if I use this code...

export const load = (async ({ params, request }) => {

  // don't run after loaded once
  if (request.url.includes('__data.json')) {
    return {
        ...
    };
  }

  // otherwise fetch data from database...
  // return fetched data...

}) satisfies PageServerLoad;

However, this won't help as the +page.ts seems to always overwrite the +page.server.ts return data in $page or PageData.


@tcc-sejohnson - Yes, I want to run two different load functions: one from the server on first load, and another from the browser on every other load. This is how the load function works now if you use only +page.ts, but unfortunately it shares the same code base. This doesn't make sense since you often can't share the same code in the browser as you can in the server (getting data from prisma, mongoose, etc). The work around is to just always load data on the server with +page.server.ts, but this is only one use case and not always efficent.

Here one example with Posts:

  1. The user goes to the home page at https://mysite.com
  2. The routes/+page.server.ts loads some data from a getAllPosts() function
  3. The home page routes/+page.svelte is loaded and displays all post (post-list) keeping them in memory (perhaps a store)
  4. The user clicks on a single posts, which navigates to https://mysite.com/posts/some-page
  5. The page routes/posts/[slug]/+page.server.ts loads the post detail data again, even though it was already in memory
  6. The page routes/posts/[slug]/+page.svelte is displayed with post detail data

As you can see here, the app is over fetching, or basically violating the n+1 rule put in other terms. For a database that charges on reads for example, this can be a big deal.

Here is how I should be able to do it:

  1. If a user goes directly to https://mysite.com/posts/some-page, it loads the +page.server.ts file as expected. However...
  2. If a user navigates to the same page after clicking on a post-list page where the data is already in memory, it doesn't need to reload the data...

That is my specific problem. There is a workaround with an endpoint, but that creates additional developer overhead and an endpoint I don't need.

However, there should be a way in SvelteKit to have two completely different load functions depending on if the request is from the browser or the server (instead of sharing a load function).

J

@elliott-with-the-longest-name-on-github
Copy link
Contributor

Yeah, all of these needs are taken care of one way or another:

  • Server load functions have an isDataRequest parameter that you can use to detect if the request is the result of a client-side nav (don't check the URL like you currently are)
  • If you have both a server load and a client load, you need to merge the responses from them:
// +page.server.ts
export function load() {
  return { foo: 'foo' }
}

// +page.ts
export async function load({ parent }) {
  const { foo } = await parent()
  return { bar: 'bar', foo }
}

If a user goes directly to https://mysite.com/posts/some-page, it loads the +page.server.ts file as expected. However...
If a user navigates to the same page after clicking on a post-list page where the data is already in memory, it doesn't need to reload the data...

The above should get you as far as you need, but architecturally, you should probably put these loads in a +layout.server.ts and a +layout.ts file that wraps the whole system.

@jdgamble555
Copy link
Author

jdgamble555 commented May 21, 2023

Ok, so you definitely got me where I needed to be after changing your code:

+page.server.ts

export const load = (async ({ isDataRequest }) => {

  if (isDataRequest) {
    return;
  }

  // fetch data
  ...
  return {
    post: data
  };
}) satisfies PageServerLoad;

+page.ts

export const load = (async ({ data }) => {

  if (browser && !data.post) {
    return {
      post: get_data_from_store()
  };
  return data;

}) satisfies PageLoad;

So, for future references, this particular problem has the above work-around solution. That being said, I still think it would be better if they were separate functions entirely.

I also digress now that I found a new issue... what if the post is not in memory because the user did not come from a post-list page, but a link from somewhere else on the site? No way to condition the +page.server.ts file to run, or send data to it? But this is a different issue, so I will post elsewhere for that.

The above should get you as far as you need, but architecturally, you should probably put these loads in a +layout.server.ts and a +layout.ts file that wraps the whole system.

Still same issue, just layout files, but I get your point.

J

@elliott-with-the-longest-name-on-github
Copy link
Contributor

Still the same issue, just layout files

No, actually! The layout load shouldn't rerun unless a dependency changes. So if you put your [slug] route's load in a +page.ts but put the posts load in a layout, as long as you don't access the url from the layout load, it won't rerun on navigations between pages "under" it.

@jdgamble555
Copy link
Author

Ah, yes very true. I don't think that helps me in this particular situation, as I need to not get the slug on route change sometimes, but good to know, thanks!

@Rich-Harris
Copy link
Member

For clarity, #10004 (comment) isn't quite right — if you have +page.server.ts and +page.ts together they should be combined like so:

// +page.server.ts
export function load() {
  return { foo: 'foo' }
}

// +page.ts
-export async function load({ parent }) {
-  const { foo } = await parent()
+export async function load({ data }) {
+  const { foo } = data
  return { bar: 'bar', foo }
}

The canonical solution here is to use an API route. On the server, event.fetch('/my-api-route') doesn't result in a network request; SvelteKit is smart enough to treat it as a regular function call, so the only overhead is converting to and from a Response. Doing it this way means you can make requests for data outside load functions, which is useful for e.g. infinite pagination — see e.g. this server load function that requests data from this API route, which also serves requests inside this event handler.

So it seems to me that the two concerns are

  • you don't want to explicitly create a new route (stress 'explicitly' — an endpoint is going to be created whatever happens)
  • you don't want to use type safety, but fetch is inherently un-type-safe

There's an idea here for typed API routes, but it sounds like you really want something like tRPC (via e.g. trpc-sveltekit) or telefunc. So to me the question is whether it's enough that the problem is solved in userland (this seems pretty nice, honestly) or whether the framework needs to introduce new primitives.

@jdgamble555
Copy link
Author

SvelteKit is smart enough to treat it as a regular function call, so the only overhead is converting to and from a Response

Very very interesting and good to know! In the end, I needed an endpoint to cover my needs. I don't want the extra overhead or configuration of TRPC when SvelteKit works just fine. But you're right, endpoint is what I needed.

Thanks!

J

@jeromecc
Copy link

Ok, so you definitely got me where I needed to be after changing your code:

+page.server.ts

export const load = (async ({ isDataRequest }) => {

  if (isDataRequest) {
    return;
  }

  // fetch data
  ...
  return {
    post: data
  };
}) satisfies PageServerLoad;

+page.ts

export const load = (async ({ data }) => {

  if (browser && !data.post) {
    return {
      post: get_data_from_store()
  };
  return data;

}) satisfies PageLoad;

So, for future references, this particular problem has the above work-around solution. That being said, I still think it would be better if they were separate functions entirely.

I also digress now that I found a new issue... what if the post is not in memory because the user did not come from a post-list page, but a link from somewhere else on the site? No way to condition the +page.server.ts file to run, or send data to it? But this is a different issue, so I will post elsewhere for that.

The above should get you as far as you need, but architecturally, you should probably put these loads in a +layout.server.ts and a +layout.ts file that wraps the whole system.

Still same issue, just layout files, but I get your point.

J

This way of coalescing data from server load and client load is nowhere to be found in the SK documentation. If found this issue after hours of googling... Thanks a lot!

@eltigerchino eltigerchino added needs-decision Not sure if we want to do this yet, also design work needed and removed needs-decision Not sure if we want to do this yet, also design work needed labels Nov 14, 2024
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

6 participants