Skip to content
This repository was archived by the owner on Jan 11, 2023. It is now read-only.

Use slots for routing #573

Closed
Rich-Harris opened this issue Feb 20, 2019 · 2 comments
Closed

Use slots for routing #573

Rich-Harris opened this issue Feb 20, 2019 · 2 comments

Comments

@Rich-Harris
Copy link
Member

Rich-Harris commented Feb 20, 2019

This is a bit of an existential mega-issue for Sapper. Will try and articulate what's in my head as succinctly as possible. (It mostly concerns internal implementation details, and would only entail minor changes to Sapper apps themselves.)

At present, nested routing is handled with _layout.svelte components that have to contain something like the following:

<svelte:component this={child.component} {...child.props}/>

Upon navigation, Sapper constructs a new tree for the current route, which looks something like this (assuming the user was visiting /foo/bar):

const props = {
  child: {
    segment: 'foo',
    component: SomeLayoutComponent,
    props: Object.assign({}, somePreloadedData, {
      child: {
        segment: 'bar',
        component: SomeLeafComponent,
        props: Object.assign({}, someMorePreloadedData, {
          child: {}
        })
      }
    })
  }
};

That data is passed to the root component, and the app redraws. If you were to navigate from /foo/bar to /foo/baz, props.child.component and somePreloadedData would stay as they are, which is nice, and if SomeLeafComponent is used for both bar and baz, we only need to re-run that component's preload function to get new data for the leaf. So in terms of preserving the component tree and minimising the amount of preload data-fetching etc, it's quite efficient.

But. Because we're applying that data at the top level, we're still setting props.child.props on the layout component, which can cause (avoidable, but surprising) side-effects, due to the component thinking it has received new state.

Then there's the child value itself, which is a Sapperism that components have to accommodate (by declaring export let child, etc). And it's a bit weird that we're using <svelte:component> for something that <slot> is conceptually better suited to.

What if we used <slot> instead?

Component-based routing

There are basically three different approaches to routing:

  • traditional Express-style routing (things like page.js and Vue router fall into this category)
  • component-based routing (React Router)
  • filesystem-based routing (Next, Nuxt, Sapper)

The advantage of fs-based routing is that you get code-splitting for free (because the framework does the complicated part for you — it's able to do so because your routes are essentially statically analysable), along with a pretty nice development experience. But if we had component-based routing, we could avoid the inefficiency outlined above:

<!-- the `foo` part -->
<SomeLayoutComponent {...somePreloadedData}>
  <!-- the `bar` or `baz` part -->
  <SomeLeafComponent {...someMorePreloadedData}/>
</SomeLayoutComponent>

Here, if someMorePreloadedData changes (as a result of a navigation from /foo/bar to /foo/baz), we needn't touch <SomeLayoutComponent> at all.

(In this case, <SomeLayoutComponent> contains <slot> instead of <svelte:component>.)

Of course, we don't have code-splitting any more. We'd need to jump through some additional hoops. And I don't want to build my apps that way, I want to use filesystem-based routing. But we can make Sapper write that stuff for us. I haven't figured out exactly what it looks like, but it feels like a promising avenue of investigation.

Bonus thoughts

  • One thing we get with the current system: you can pass props from a layout component to a leaf component (or child layout component). Not sure what that looks like in this world (context API, perhaps?)
<svelte:component this={child.component} {...child.props} answer={42}/>
  • People have often asked for an official Svelte router. Maybe this dovetails with that — could we generate a component that implemented Sapper's filesystem-based routing using a hypothetical @sveltejs/router package?
  • This would be even nicer with Suspense (don't worry, not going to put Svelte 3 on hold until we can implement it)
@Rich-Harris
Copy link
Member Author

My best attempt at figuring out what the generated component would look like so far:

<script>
  import Layout from '../../../routes/_layout.svelte';
  import Error from '../../../routes/_error.svelte';

  export let error;
  export let level0;
  export let level1;
  export let level2;
</script>

<Layout segment={level0.segment} {...level0.data}>
  {#if error}
    <Error {...error}/>
  {:else}
    <svelte:component this={level1.component} segment={level1.segment} {...level1.data}>
      {#if level2}
        <svelte:component this={level2.component} {...level2.data}/>
      {/if}
    </svelte:component>
  {/if}
</Layout>

This is for an app with a) a root _layout.svelte component, and b) at least one route with an intermediate layout component. If there was no root layout component it could look like this instead:

<script>
  import Error from '../../../routes/_error.svelte';

  export let error;
  export let level0;
  export let level1;
</script>

{#if error}
  <Error {...error}/>
{:else}
  <svelte:component this={level0.component} segment={level0.segment} {...level0.data}>
    {#if level1}
      <svelte:component this={level1.component} {...level1.data}/>
    {/if}
  </svelte:component>
{/if}

Conversely, if it had one or more routes with two intermediate layout components (probably unusual, but there's no limit to the number of layout components you can have) then it'd look like this:

<script>
  import Layout from '../../../routes/_layout.svelte';
  import Error from '../../../routes/_error.svelte';

  export let error;
  export let level0;
  export let level1;
  export let level2;
</script>

<Layout segment={level0.segment} {...level0.data}>
  {#if error}
    <Error {...error}/>
  {:else}
    <svelte:component this={level1.component} segment={level1.segment} {...level1.data}>
      {#if level2}
        <svelte:component this={level2.component} {...level2.data}>
          {#if level3}
            <svelte:component this={level3.component} {...level3.data}/>
          {/if}
        </svelte:component>
      {/if}
    </svelte:component>
  {/if}
</Layout>

Upon navigation, Sapper would do something like this pseudo-code:

const data = {};

await Promise.all(route.levels.map(async (level, i) => {
  if (changed(level, i)) {
    const mod = await level.load();
    data[`level${i}`] = {
      segment: segments[i + 1],
      component: mod.default,
      data: mod.preload ? await mod.preload(...) : {}
    };
  }
});

app.$set(data);

@Conduitry
Copy link
Member

Implemented some time ago in #574.

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

No branches or pull requests

2 participants