sveltejs / kit

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

Page transitions in layout don't works well using the `page` store #7877

Open Tal500 opened 1 year ago

Tal500 commented 1 year ago

Describe the bug

We all like Svelte transitions, and some of us are using them for page transitions. A good repo. example for page transitions is this one, by basically doing something like this code in the main layout:

{#key pathname}
    <div in:fly={{ x: -5, duration: 500, delay: 500 }} out:fly={{ x: 5, duration: 500 }}>
        <slot />
    </div>
{/key}

The repo. I mentioned, is passing the pathname in the load function of the layout, so in their code, pathname equals to data.pathname. The disatvantage of this method, is that the main layout load function will be invalidated every time the page is changing, which is probably very wasteful.

The obvious solution is to use the page store instead, and substitute in pathname the value of $page.url.pathname instead. However, the transition fails, and it looks like the page store is being updated too late, causing the transition to take place after the slot has already been changed, resulting with the effect that the new page is also rendered on the outro transition:

https://user-images.githubusercontent.com/1467072/204525557-1fa4658f-3129-4c8f-b665-355c1f420f8b.mp4

Upgrading the svelte and the @sveltejs/kit packages results with the same effect.

Reproduction

  1. Clone the repo. from before, npm install and npm run dev. You'll see everything works well.
  2. Now in https://github.com/evanwinter/sveltekit-page-transitions/blob/main/src/routes/%2Blayout.svelte#L14, change the row from <PageTransition pathname={data.pathname}> to <PageTransition pathname={$page.url.pathname}>, and of course import the page store by import { page } from '$app/stores';. You'll see now the same results as in the video above.

Logs

No response

System Info

System:
    OS: Windows 10 10.0.19044
    CPU: (4) x64 Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz
    Memory: 4.38 GB / 15.49 GB
  Binaries:
    Node: 18.3.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.18 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 8.14.0 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.19041.1266.0), Chromium (107.0.1418.62)
    Internet Explorer: 11.0.19041.1566
  npmPackages:
    @sveltejs/adapter-netlify: ^1.0.0-next.72 => 1.0.0-next.72
    @sveltejs/kit: ^1.0.0-next.436 => 1.0.0-next.436
    svelte: ^3.49.0 => 3.49.0
    vite: ^3.0.9 => 3.0.9

Severity

serious, but I can work around it

Additional Information

No response

dummdidumm commented 1 year ago

I don't think there's anything we can do here. The page store only updating after the navigations completes is the correct behavior. To do what you want it's probably better to use the $navigating store to map all changes from null to <something> to a key that changes every time that happens.

This sounds like something for an examples or recipes section.

Tal500 commented 1 year ago

I don't think there's anything we can do here. The page store only updating after the navigations completes is the correct behavior. To do what you want it's probably better to use the $navigating store to map all changes from null to <something> to a key that changes every time that happens.

This sounds like something for an examples or recipes section.

Thanks for the workaround! If it helps, the key I used was ($navigating?.to ?? $page.url).pathname, but it have the problem that it can animate a page entrance even when the page wasn't fully loaded, resulting the same issue but the oppoisite - now on heavy load, the old page will entrance as well, and suddenly, when the loading finish, the new page will be replaced immediately.

For fixing this potential behavior, you need to do both use key and if blocks with navigation:

{#key $navigating}
    {#if !$navigating}
        <div in:fly={{ x: -5, duration: 500, delay: 500 }} out:fly={{ x: 5, duration: 500 }}>
            <slot />
        </div>
    {/if}
{/key}

This will make the previous page to always be transitioned out, but the new page will not be transitioned in before the navigation is complete.

Side effect of the solution: if the loading takes X milliseconds to complete, the transition in in this code will be only after 500 + X milliseconds because of the delay, when it should have been max(500, X) instead. A better solution will be to use the following code instead (where outroFinished is a variable initialized to true):

{#if !$navigating && outroFinished}
    <div transition:fly={{ x: 5, duration: 500 }} on:outrostart={() => { outroFinished = false; }} on:outroend={() => { outroFinished = true; }} >
        <slot />
    </div>
{/if}
jrmoynihan commented 1 year ago

@Tal500 @dummdidumm My solution for this was to simply use the afterNavigate() lifecycle hook:

// +layout.svelte

<script>
   import { afterNavigate } from '$app/navigation'

   let show_special_links = false

  afterNavigate(async (nav) => {
      const { from, to } = nav;
      show_special_links = to.url.href.includes('/special-address');
  });
</script>

<Nav>
    {#if show_special_links}
        <SpecialNavLinks />
    {/if}
</Nav>
Tal500 commented 1 year ago

@Tal500 @dummdidumm My solution for this was to simply use the afterNavigate() lifecycle hook:

// +layout.svelte

<script>
   import { afterNavigate } from '$app/navigation'

   let show_special_links = false

  afterNavigate(async (nav) => {
      const { from, to } = nav;
      show_special_links = to.url.href.includes('/special-address');
  });
</script>

<Nav>
    {#if show_special_links}
        <SpecialNavLinks />
    {/if}
</Nav>

Notice that this solution may suffer from the side effect I mentioned in my previous comment.

jrmoynihan commented 1 year ago

Notice that this solution may suffer from the side effect I mentioned in my previous comment.

Agreed, but for most use-cases, I think it's going to fit the intended effect with more consistent/predictable behavior for your average user.

Laaouatni commented 1 year ago
<script>
   import { navigating } from "$app/stores";
</script>

{#key $navigating}
  {#if !$navigating}
    <main
      transition:fly={{
        x: window.innerWidth,
        duration: 1000,
        easing: elasticInOut,
      }}
    >
      <slot />
    </main>
  {/if}
{/key}

thanks @Tal500, that worked very fine!