kit: Navigating to new rest parameters in route does not cause page to re-render

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 https://github.com/sveltejs/kit/issues/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.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 17 (7 by maintainers)

Most upvoted comments

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.

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!

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;).

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.