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

Remove $page.stuff and allow load functions to run concurrently #4911

Closed
Rich-Harris opened this issue May 13, 2022 · 20 comments
Closed

Remove $page.stuff and allow load functions to run concurrently #4911

Rich-Harris opened this issue May 13, 2022 · 20 comments

Comments

@Rich-Harris
Copy link
Member

Rich-Harris commented May 13, 2022

Describe the problem

I've come to the view that stuff (as it's currently designed) is a mistake, because it forces nested routes to load serially and because it prevents us from taking advantage of nested routes to stream responses (see #4910).

Ideally if we had a route like this...

<!-- src/routes/__layout.svelte -->
<script context="module">
  export async function load({ fetch }) {
    const res = await fetch('/one.json');
    const { one } = await res.json();

    return {
      props: { one }
    };
  }
</script>

<script>
  export let one;
</script>

<h1>{one}</h1>
<div>
  <slot/>
</div>
<!-- src/routes/foo/__layout.svelte -->
<script context="module">
  export async function load({ fetch }) {
    const res = await fetch('/two.json');
    const { two } = await res.json();

    return {
      props: { two }
    };
  }
</script>

<script>
  export let two;
</script>

<h2>{two}</h2>
<div>
  <slot/>
</div>
<!-- src/routes/foo/bar.svelte -->
<script context="module">
  export async function load({ fetch }) {
    const res = await fetch('/three.json');
    const { three } = await res.json();

    return {
      props: { three }
    };
  }
</script>

<script>
  export let three;
</script>

<p>{three}</p>

...then two things would happen. Firstly, we'd fetch one.json, two.json and three.json concurrently. Secondly, as soon as one.json returned, we'd flush

<h1>1</h1>
<div>

then as soon as two.json returned, we'd flush

<h2>2</h2>
<div>

then as soon as three.json returned, we'd flush the remainder:

<p>3</p>
</div>
</div>

At present, that's not possible. We run load functions in sequence for two reasons...

  1. we want to bail out early if a layout errors or causes a redirect — subsequent load functions should not run
  2. a nested route should be able to access any stuff that was returned from an rootward layout

...and we can't render anything until everything has loaded, because the root layout might make use of $page.stuff.

Ideally, we'd be able to solve all these problems without relying on a design that forces serial loading and delayed rendering.

Describe the proposed solution

Honestly, I haven't really figured this out yet — I'm really just trying to articulate the problem in hopes that a solution will present itself. But here's a straw man:

<script context="module">
  export async function load({ fetch, parent }) {
    const stuff = await parent(); // merges stuff from all parent layouts

    return {
      stuff: {
        b: stuff.a + 1
      },
      props: (stuff) => ({
        // because `props` is a function, rendering is
        // delayed until we have the final `stuff` object
        title: stuff.title
      })
    };
  }
</script>

<script>
  // this comes from the leaf component
  export let title;
</script>

<svelte:head>
  <title>{title}</title>
</svelte:head>

This doesn't feel totally intuitive, but it would speed up performance in the common case where load isn't blocked on its parents, and $page.stuff isn't used. And we would still have the ability to bail out if a parent load redirects (maybe await parent() throws an error for non-200 responses?)

Alternatives considered

I am all ears. (I'm also interested to know how people are using stuff)

Importance

would make my life easier

Additional Information

No response

@TheHadiAhmadi
Copy link

Hi @Rich-Harris
I use $page.stuff because this is useful to reduce boilerplate code.
I store some data (which comes from backend after successful login) in stuff that I need it in pages, for example apiKeys or enum types....

Describe the proposed solution

I think one solution is to make stuff a function (same as props in your example) which it's first parameter is merged stuff from parent layouts

export async function load({ fetch }) {
   const res = await fetch('/some-data').then(res => res.json())

    return {
      stuff: (stuff) => ({ // merged stuff from parent layouts
        b: stuff.a * res.multiplier,
        apiKey: res.apiKey
      }),
      props: (stuff) => ({ // final stuff object
        title: stuff.title
      })
    };
  }

@Zachiah
Copy link

Zachiah commented May 13, 2022

Since svelte is already a compiler could you detect if $page.stuff depends on other $page.stuff and only then run them in sequence, otherwise just run them in parallel? I think for most cases it isn't very usefull to have $page.stuff depend on other $page.stuff.

From my view, the purpose of $page.stuff is to pass information up to the parent so if we have a structure

a/
    __layout.svelte
   [paramB]/
       __layout.svelte
       c/
           [paramC].svelte

the reason stuff is useful is that a/__layout.svelte doesn't have access to paramC right?
well a/b/c/[paramC.svelte] could probably have access paramB

so if a/__layout.svelte expects a title in $page.stuff
and a/[paramB]/__layout.svelte wanted to set it to ParamB = ${paramB}; ChildrenStuff: ${page.stuff.title}
then a/[paramB]/c/[paramC].svelte sets $page.stuff.title to ParamC: ${ParamC}

this would be a case where having the dependancy tree makes sense, however this could easily achieved without the dependancy tree simply by putting
ParamB = ${paramB}; ChildrenStuff: ParamC: ${ParamC} in a/[paramB]/c/[paramC].svelte

So the only way this wouldn't actually be rewritten without the nesting is if the parents fetch data and put it in params.stuff and you don't want to duplicate that fetching in a/[paramB]/c

I think one way to get around the series fetching in this case would be to just allow paramC to be accessed from a/[paramB]/__layout.svelte if it's available of course.

In a perfect world I guess only one fetch function would be run per route if this is an initial page load and the fetch function could determine how the parallelism is handled (Promise.all). Of course this isn't really possible because it doesn't make sense to reload the parent layout's data when ever the child changes which is why allowing paramC in the a/[paramB]/__layout.svelte has drawbacks. I guess again you could detect if children params are used inteligently and only reload the parent layout on children changes if it is necessary.

Let me know if I'm completely off here

@Rich-Harris Rich-Harris added this to the 1.0 milestone May 13, 2022
@Mlocik97
Copy link
Contributor

I typically pass some meta data through stuff from leafs components or better said pages to upper layouts, where I can use them. I even pass title of page through it.

@baukevanderlaan
Copy link

(I'm also interested to know how people are using stuff)

I use stuff only as a replacement for store to pass page meta data to __layout‘s that are higher up in the tree. Examples are breadcrumb components, SEO components and locale switchers.

I‘d still welcome a solution in which these things could be done in SSR with a simple store because store is such an elegant concept clientside and I think it’s a bit of a bummer that this is one of the few situations in the Svelte ecosystem where we as developers need to consciously change our paradigm when making an app work for SSR. I'm sure changing store to work on SSR and making it ready for streaming HTML is probably much harder than any other solution you come up with, but I still wanted to mention my sentiment now that stuff is up for debate. Feel free to mark as off-topic!

@stephane-vanraes
Copy link
Contributor

I use stuff quite a lot in my current project, where the layout loads data from an api, that is common to all pages within that hierarchy and passes it on to the page. This reduces the number of API calls drastically and speeds the app up a lot.

This should still be possible to do in any new situation.

@plmrry
Copy link

plmrry commented May 18, 2022

Also using stuff a lot in a project.

To me it seems like the features of stuff can be broken up into:

  1. A mechanism to pass loaded data from a __layout into nested routes
  2. A convenient global store available via $page.stuff
  3. "A mechanism for pages to pass data 'upward' to layouts." (from the docs)

It seems like 2 and 3 are mostly a convenience that could be done in user-land via a store + context, if I'm not missing something.

@endigma
Copy link

endigma commented May 19, 2022

I'm using this for breadcrumbs atm, if this is going to be removed please document how to replace this "upwards push" functionality elsewhere? I'd rather not have to re-engineer this system because of a svelte update.

@Rich-Harris
Copy link
Member Author

It seems like 2 and 3 are mostly a convenience that could be done in user-land via a store + context, if I'm not missing something.

Almost. It could be made to work in a client-side situation, but not during SSR — by the time you set the store value in the leaf, the layout would already have been rendered.

The only way to get that data to the layout in time for it to be SSR'd that I can see is a) with a SvelteKit-managed store (like $page.stuff) which can be populated before rendering happens, or b) to somehow get the data into the layout's props. The proposal above is one approach to option b:

// we can render this layout immediately, because it returns a props object
return {
  props: {
    answer: 42
  }
};
// we wait to render this layout until we have accumulated `stuff` from the child layouts and the leaf
return {
  props: (stuff) => ({
    answer: 42,
    breadcrumbs: stuff.breadcrumbs
  })
};

If you do the first option, you opt out of the ability to read child stuff when rendering layouts. If you do the second option, you opt out of the ability to render in phases. I think making this trade-off an explicit (and granular — per-layout, rather than per-app) option is the right approach — maximum performance by default, maximum flexibility by choice.

@coryvirok
Copy link
Contributor

coryvirok commented May 24, 2022

I only just recently understood what stuff could be used for. I'm now using it to pass data "up" to layouts but that definitely was a fairly unintuitive thing to learn.

Reading this thread, I'm reminded of how DOM events have 2 phases, bubbling and capture, to handle this type of logic where we want to push data up through the hierarchy of elements as well as annotate an event while it's being pushed down through the hierarchy.

Maybe there's similar event pattern we can make use of for stuff?

@Algoinde
Copy link
Contributor

Algoinde commented May 27, 2022

One thing to consider for nested layouts: if one of the layouts/endpoints errors out, do we throw the whole chain under the bus or cancel out only the layout stack at and below the error, and return (in SSR) only what succeeded?

@benmccann
Copy link
Member

benmccann commented May 27, 2022

I like the idea of running the load functions concurrently unless one depends on another.

I think there might be alternatives to removing $page.stuff that are nearly as performant. Not being able to pass data upwards in pages seems like a severe restriction and would be a tough thing to lose. Hopefully we could get the benefit in other ways. If you think about the things that get sent to the server sooner by streaming, we could send it earlier or eliminate a lot of it in other ways. The layout contains a few things we could potentially stream:

  • serialized data for hydration
  • head contents
  • HTML for the top part of the page (typically a logo, menu bar, etc.)

A lot of the serialized data for hydration could be dropped if we introduced something like partial hydration. This is obviously not something that would happen in the near future, but Svelte's always avoided taking API shortcuts and focused on the long-term.

In terms of head contents, the most valuable thing to stream is the external resources, so that the browser can request them earlier. This could be done by streaming Link headers (earlier discarded PR) or better yet with 103 early hints (which will ship with the next version of Chrome, but likely work only in adapter-node, adapter-deno, and other non-serverless platforms - discussion).

The HTML for the top of the page could only be sent if we truly supported streaming. The HTML itself is unlikely to be that large, but may link to larger resources like images. Those could potentially be handled in a similar way to the head contents - though we'd probably want to let the user select which should be preloaded (draft PR for font preloading)

The transformPage API would probably need to change even with these other optimizations (#4910)

@coryvirok
Copy link
Contributor

What if Layouts were able to receive all of the props of their children as slot props? This would remove the need for stuff and would rely on slots to pass data up and down from page to layout or from layout to layout.

<!-- __layout.svelte -->
<script>

  // These come from whatever page is rendered in <slot/>
  // and are also available as slot props to a parent layout
  export let user
  export let navLinks

  // These are passed down to pages
  let var1 = 'some data from the layout'
  let var2 = 42
</script>

<header>
  Hello {user.name}
</header
<nav>
  {#each navLinks as navLink}
    <a href={navLink.href}>{navLink.text}</a>
  {/each}
</nav>

<main>
  <!-- 
      user and navLinks are passed "up" to the layout from the page. 
      var1 and var2 are passed "down" to the page from the layout.
  -->
  <slot {bind:user} {bind:navLinks} {var1} {var2} />
</main>

@Algoinde
Copy link
Contributor

Algoinde commented May 29, 2022

So not sure if this is related, but it seems like it is. I'm trying to do a new project and ran into a problem with how things currently are: nested layouts which have a load function rerun every time you navigate to a lateral route instead of just keeping the data.

Here's the example layout:
image

Each level of the route structure has a __layout in it that contains the markup and the load to populate that markup with data using API requests.

Say, you were on /profile/2/foo and navigate to /profile/2/bar using href. This requests /profile, then /profile/2, and then renders the /profile/2/bar, every time the path is changed.

I'm very new to SvelteKit so maybe there is a way to make this work as things currently stand? If not, it should definitely be factored in.

@tsukhu
Copy link

tsukhu commented Jul 8, 2022

Hi @Rich-Harris ,

stuff helped me to reorganize my code and reduce redundancy.
My scenario

  • I have multiple routes that depend on initialization of the state.
  • Its a dashboard with widgets , layouts etc. , so a dynamic route will need this initialized as well as the fixed routes where these are being accessed.
  • Earlier this was duplicated in the sense that the route which was first called would initialize the state and the other routes would then use the same state. This was done by multiple load functions (per route) take care of loading the content from the API and initializing the state.
  • Instead now with stuff , I can set this up in the layout and access the same in the different routes and dynamic routes and access them in the load functions of the specific routes

Some psuedo code in the __layout.svelte file

export async function load({ fetch, session }) {
...
	try {
			const res = await Promise.all([
				fetch(`/api/db/registry.json`),
				fetch(`/api/db/urlConfig.json`),
				fetch(`/api/db/myapi.json`),
				fetch(`/api/db/dashboardsConfig.json`),
				fetch(`/api/db/somemore.json`)
			]);
			result = await Promise.all(res.map((r) => r.json()));
		} catch (e) {
			console.log(e);
			return { stuff: { widgets: {}, urls: [], transforms: [], dashboards: [], imports: {} } };
		}

		// you can pass the `articles` via props like that
		return {
			stuff: {
				widgets: result[0],
				urls: result[1],
				transforms: result[2],
				dashboards: result[3],
				morestuff: result[4]
			}
		};
...
}

and now in the specfic routes pages access it in the load and pass it down

<script context="module" lang="ts">
	
	export async function load({ stuff, params }) {
		return {
			props: {
				data: stuff.widgets,
				urls: stuff.urls,
				slug: params.slug,
				transforms: stuff.transforms,
				morestuff: stuff.morestuff
			}
		};
	}
</script>

Here is the logic which is primarily client side only (Depends on initialization of svelte stores on initialization)

  • Multiple routes require the configuration from multiple apis to be available and are then used to initalize the state (svelte stores) .
  • If a store is already initialized by another route then we don't need to do anything otherwise that route/page will initialize that part of the state it uses/requires
  • stuff helps us in not having to have these fetch commands in multiple load functions and we just pass it down.

@eallenOP
Copy link

eallenOP commented Jul 8, 2022

Since this thread has been revived I'll weigh in because I've been learning stuff for my current project and came to quite enjoy it. I have come from SSGs so I use stuff like you would use Jekyll front matter: to pass all manner of things from pages up to layouts.

Specific example:

I have an app that displays a team's members on one page and clicking a person navigates to that person's profile page. Standard stuff (no pun intended, honest). Also standard stuff is passing the title of the page to its layout to be used in a heading and styled, obviously, by the layout.

I have a Sidebar component in my top layout that needs different links depending on which page you're on. One set of links on the team list page, a different set on the person page (and other pages). These links I send via stuff from the page (route) files to the layout and into the Sidebar props.

The sidebar also has an image at the top which is the person's profile pic on the person page and the team's logo on the team page. These come from data fetched by endpoints that relate to the pages, and are passed up to the layout in stuff and then into the Sidebar props. Same with displaying the person's/team's name in the Sidebar beneath the picture.

Once I got stuff up and running I found myself sticking more and more... stuff... in there with a feeling of glee at how easy it is to kind of fling stuff around :)

If I couldn't pass this data up, I would have to repeat the Sidebar over and over again on each page. Then what would be the point in having a layout if it doesn't keep my code dry?

Having said that, I would happily do it with slot props or something else. I find $page.stuff to be quite tidy though rather than a lot of slot attributes hanging around.

@dummdidumm
Copy link
Member

The more I think about this the more I like the propsal - $page.stuff feels too big of a footgun. It's too easy to just attach all kinds of stuff (haha) too it, which is then available everywhere, kind of like a global blob. It's also not very TypeScript-friendly because you type it for the whole app, so you have to be extra careful whether or not the thing you want to access actually exists on this route.

The majority of people seem to use $page.stuff to pass data upwards to layouts, which this proposal achieves in a - in my opinion - cleaner way. There's also people using it to pass down shared data and/or to get a global-like store (also see discussion in #4339), which would become a little more cumbersome, (need to create a load function and merge stuff into props or set a store into the context yourself). This is somewhat related to a prop drilling issue some people have with pages (passing their props through many components).

Possible solution: $page.stuff becomes $stuff which is different because it won't contain stuff from components below it (which will be a huge drawback for those using current $page.stuff as a global store and want to update the shared global from every component). Similarly, props could be $props (only for leaf nodes, not sure how I feel about layout components contributing to it [what about shadowed props?]). Implementation-wise these would be stores stored in context.

@Rich-Harris
Copy link
Member Author

Closing, as stuff was removed in #5748. load functions run concurrently (but can await data from parents via await parent(), and merged data is globally available as $page.data.

@opensas
Copy link

opensas commented Jun 19, 2023

@dummdidumm sorry, perhaps I missed something, what would be the current recommended way to pass information from +page.svelte to +layout.svelte` now that we no longer have page.stuff? You mentioned that this proposal handles it in a cleaner way but I seem to have missed it. Thanks.

@dummdidumm
Copy link
Member

As Rich pointed out in his last comment, use $page.data instead - it contains the accumulated data of all load functions. And if you need things from the parent load function, use await parent()

@opensas
Copy link

opensas commented Jun 19, 2023

Thanks a lot for you (instant!) reply. I was going to try this (basically setting a store with context in the layout) but I still don't quite get the pros/cons of each approach. I'm trying to do simple stuff like changing the title or a class of and element in the layout from a page component. I'll give a try to both approaches. Currently I solved it with a shared store between the layout and the page.

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

No branches or pull requests