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

Navigating to new rest parameters in route does not cause page to re-render #3884

Closed
thegrumpysnail opened this issue Feb 13, 2022 · 18 comments

Comments

@thegrumpysnail
Copy link

Describe the bug

Let me preface this by saying that I'm very new to svelte and svelte kit, so maybe I'm doing something dumb. I'm doing something similar to #643, although that issue seems to be closed without an obvious resolution.

Basically, if I have /some-path/[...slug].svelte, I can do -

/ -> /some-path/first-path

But when I am on /some-path/first-path and I try to navigate to /some-path/second-path ...

The url changes as expected, but the page itself doesn't realize it should be re-rendering, so the contents stay the same. A refresh of the page does load the new page as expected.

Reproduction

https://stackblitz.com/edit/sveltejs-kit-template-default-mppqbl

Click on 'Test'
Click on 'Works Ok' - Navigates to a different route as expected and renders 'first-path'.
Click on 'Home'
Click on 'Test'
Click on 'Broken' - Nothing happens.

I would expect 'another-path' to be displayed on the page.

Logs

No response

System Info

npx: installed 1 in 0.614s

  System:
    OS: Linux 5.11 Ubuntu 21.04 (Hirsute Hippo)
    CPU: (8) x64 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz
    Memory: 42.21 GB / 62.60 GB
    Container: Yes
    Shell: 5.1.4 - /bin/bash
  Binaries:
    Node: 14.17.5 - ~/.nvm/versions/node/v14.17.5/bin/node
    npm: 6.14.14 - ~/.nvm/versions/node/v14.17.5/bin/npm
  Browsers:
    Chrome: 97.0.4692.99
    Firefox: 96.0
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.17 
    @sveltejs/kit: next => 1.0.0-next.267 
    svelte: ^3.44.0 => 3.46.3

Severity

annoyance

Additional Information

I didn't really know what to put for severity. Surely I'm not the first person to have run into this issue, so I assume I'm doing something silly, or there is a way to work around it.

@raprocks
Copy link

you are doing it all wrong.
you need a load function that accepts the params and then capture those in the page.

<script context="module">
export async function load({ params, fetch, session, stuff }) {
  return {
			status: 200,
			props: {
				paramData: params.nested
			}
		};
}
</script>
<script>
  export let paramData;
</script>

this is how it should look like. this is not a bug.

@PH4NTOMiki
Copy link
Contributor

Duplicate #3727

@thegrumpysnail
Copy link
Author

Ahhh yes! I see now @raprocks! Ok, so that part seems to be working as expected. I guess the behavior I was seeing was actually a little bit different from what I described initially.

So the path changes work as expected, but let's say I do something like -

<script context="module">
  ... stuff here that you wrote ...
</script>

<script>
  import { wriable } from 'svelte/store';
  export let paramData;

  let paramDataUppercase = writable(paramData.toUpperCase());
</script>

<p>{ paramData } - { $paramDataUppercase } </p>

Let's say we navigate to /test, it executes the script and it would display test - TEST. If I navigate to say /test-again, it would render test-again - TEST. The second <script> never re-executes, so the value of paramDataUppercase remains the same. Should I be doing the logic to generate paramDataUppercase inside of the load function and pass that as a prop instead of attempting to derive that data in the client?

Like I said, I'm very new to svelte, so some of the nuances are still lost on me. I guess I'm imagining a scenario where navigating within the SPA may want to use data that might be available in the stores, which aren't available from the server-side script.

@raprocks
Copy link

if its a writable store then you should update it rather than creating a new one everytime. Also store it in some js module and import it and subscribe to it. On mobile right now so cannot try. Will let you know as soon as i do if that works.

@thegrumpysnail
Copy link
Author

Alright, I think I'm starting to understand more. If I want something inside of the <script></script> to execute again after the parameters change, should I do something like -

import { page } from '$app/stores';

page.subscribe(() => {
  ... logic here ...
});

I'm guessing the <script></script> only executes when the component renders initially, but not when the props change. So I would have to listen to the page store change instead if I want it to re-render, right? If that's the case, I think this issue can be closed.

@Rich-Harris
Copy link
Member

Not your fault, this was a bug in a recent version of Kit — fixed by #3925!

@thegrumpysnail
Copy link
Author

Thanks for the reply @Rich-Harris! Admittedly, I might be even more confused now because I'm still seeing the original issue with 272.

<!-- The load function runs every time I navigate to a new page -->
<script context="module">
  export const load = async ({ params }) => ({
    props: {
      someData: params.test,
    },
  });
</script>

<!--

Should this whole script block execute every time a new subsequent page that
matches this pattern is visited? Or only the first time it renders from the server, and
then the next time it renders on the client?

-->
<script>
  export let someData;

  let someDataUppercased = someData.toUpperCase();
</script>

<main>
  <h1>{ someData } &gt; { someDataUppercased }</h1>
  <a href="/testing/foo">foo</a>
  <a href="/testing/bar">bar</a>
</main>

Before trying to replicate again using the template (I'm not even sure which version of sveltekit it's using), I just want to see if my understanding is right, or if I should be subscribing to the page store inside of the <script></script> block for subsequent renders. It seems to me that if the context="module" script block executes every time, so should the regular <script></script> block.

Thanks!

@mrkishi
Copy link
Member

mrkishi commented Feb 16, 2022

Note that #3925 hasn't been published yet (it's not even in next.272).

@thegrumpysnail
Copy link
Author

Hmm, alright, maybe I was reading the commit history incorrectly then, my apologies @mrkishi. Since #3925's commits included the changelog change present in https://github.com/sveltejs/kit/releases, I guess I assumed incorrectly. I'll hold off on testing again then.

@yousufiqbal
Copy link
Contributor

I'm also facing this issue.

@rmunn
Copy link
Contributor

rmunn commented Feb 17, 2022

Looks like #3925 has been published now, in next.275. Please update to Svelte-Kit version next.275 or later and re-test, then close the issue if the problem has been fixed. Thanks!

@thegrumpysnail
Copy link
Author

Unfortunately, as of 1.0.0-next.276, I still seem to be experiencing the same behavior. I'll update the stackblitz environment above later today to double check just in case it's something weird with my local environment.

@thegrumpysnail
Copy link
Author

I updated the stackblitz environment and was able to replicate the same behavior I encountered before, so I suspect #3925 did not fix this particular issue.

@rmunn
Copy link
Contributor

rmunn commented Feb 20, 2022

I've played around with your stackblitz setup, and I believe I understand what's going on. If I'm right, this is not a bug, but a case of you not yet understanding how Svelte is supposed to work. So I'll try to explain it. But first, a quick TL;DR:

Replace let uppercase = paramData.toUpperCase() with $: uppercase = paramData.toUpperCase() and your code will work as you expect it to.

Now to go into more detail to explain. For starters, I'll answer the question you asked in the code sample of this issue comment:

<!--

Should this whole script block execute every time a new subsequent page that
matches this pattern is visited? Or only the first time it renders from the server, and
then the next time it renders on the client?

-->
<script>
  export let someData;

  let someDataUppercased = someData.toUpperCase();
</script>

The answer is that it's not page visits that cause the <script> block to be re-executed, but whether the component is destroyed and re-created. The <script> block is compiled into the initialization code (the "constructor", in OOP terms, though Svelte components don't exactly behave like classic OOP objects) for the component. So it will be run every time a new instance of the component code is created. The <script context="module"> block is compiled into code that lives at the root level of the Javascript module. It's run only once the first time the module is imported, and stays in memory and is not re-run. Except for the load() function, which gets called multiple times as the documentation explains. But the rest of the <script context="module"> code is not re-run. If you put a console.log('Initializing module') call into the module script, it will only be logged to the console once. (SSR can make it look like it's being run twice, but it's being run once on the server and then once in the client; it will not run a second time on the server, nor a second time on any given client.)

Now, the thing that's causing you some confusion is that in Svelte-Kit, pages are also components. And when you put an <a href="/path"> link in your site, Svelte-Kit rewrites that link to use the Svelte-Kit navigation code to handle the link. That code will, among other things, correctly unmount and recreate any components that need to be removed from or added to the page. And, crucially for this example, if the link points to the same page, or even if it points to a different URL that would be rendered by the same page component (as in this case), Svelte-Kit does not tear down and recreate the page component.

Which means that the <script> block is not re-run the second time you click on the link, only the first time. So the let uppercase = paramData.toUpperCase() statement only runs once when the page is loaded (because the page component is being created), but it doesn't run again when you click on a different link that leads to the same page. If you had these two links in your index.svelte, though, they would both show the uppercase data you expect:

<a href="/test/first-path">Test One</a>
<a href="/test/second-path">Test Two</a>

And now for the $: bit. As you probably remember from the tutorial, the $: marker marks a reactive declaration or a reactive statement (if it's in the form $: variableName = someDefinition then it's a reactive declaration, otherwise it's a statement). A normal let declaration is run only once, when the component initializes, but a $: declaration is run every time its dependencies change. So by writing $: uppercase = paramData.toUpperCase(), you're declaring the uppercase value as one that depends on paramData, and will call .toUpperCase() on the new value of paramData every time paramData changes.

And since the load() function re-runs every time the page params change, as per the documentation, that means that the paramData prop is assigned a different value every time the page params change. So as long as you've declared uppercase with the $: syntax, it too will be assigned a different value every time the page params change.

And that's why I said that this is not a bug, but a case of you not yet understanding how Svelte is supposed to work. Because you declared uppercase with a standard let declaration, but expected it to change every time paramData changed. Svelte can do that, but you have to explicitly opt in to that behavior by using the $: syntax. Hence the solution I posted at the top of this comment, which I'll put down here as well:

Replace let uppercase = paramData.toUpperCase() with $: uppercase = paramData.toUpperCase() and your code will work as you expect it to.

@thegrumpysnail
Copy link
Author

First off, thank you so much for the detailed explanation @rmunn. So that's similar to what I mentioned earlier where I can also subscribe to the page store for the updates, right?

Alright, I think I understand better now. It's still a little confusing coming from a React background, where really any reference change would result in a re-render. For example, I assumed that since the props changed, it would result in a re-render of the component, even if it was already mounted (or whatever svelte's equivalent terminology is). But I'm also seeing similar behavior elsewhere that are not related to route changes.

For example, if I render a component and I pass in an object (as opposed to a string, which doesn't seem to have the same issue), I have to specifically make the script reactive and listen to the props change. While subscribing to stores made sense, I would have assumed a prop change would automatically cause the component to re-render. But it seems I have a lot more to learn about svelte.

Thank you again, I'll close this bug report then since it sounds like this is expected behavior, and as I suspected, a limitation of my knowledge. At minimum, I think it might be helpful if at some point the svelte kit documentation could have some sort of documentation explaining some of this behavior, much like your wonderfully detailed explanation.

@mrkishi
Copy link
Member

mrkishi commented Feb 20, 2022

Just a nitpick: when a prop changes, the component does re-render. But in Svelte, the main script tag runs on component instantiation, unlike a React render function that gets called repeatedly. If you have a prop (export let object;) and you use it on your template (<span>{object.someValue}</span>), the component will update accordingly whenever it changes. It's just that if you have a computed value from that prop (const double = object.someValue * 2;), that will only run once, so you need to make the statement reactive ($: double = object.someValue * 2;).

@thegrumpysnail
Copy link
Author

Yeah, it's funny that you replied, @mrkishi - I was just rubber duckying to my wife as she nodded along when I suddenly understood what was happening. I normally destructure the prop that's passed in, and I realized that the destructuring only happens once. So you have to either a) reference the object attribute directly or b) set the destructured property as reactive.

Thank you for confirming my suspicions about that too!

@cliffordkleinsr
Copy link

Additional documentation for those still facing this issue:

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

8 participants