Repository: sveltejs/kit (js)

Issue:
Load requires parameter to exist in order to be tracked
### Describe the bug

Sveltekit requires the url parameter to be defined in order for it's usage to be tracked on the load function

I am guessing because it sets a getter tracking the parameter usage for all _defined_ parameters, but if a parameter is not defined, it won't track the parameter being used, and fails to re-run if it eventually gets defined without any other parameter changing

this is not inherently an issue for normal pages, but for layouts this issue is apparent

### Reproduction

root \_\_layout logging when Load runs
have a route `[first]` and another route `[first]/[second]`
travel to route `[first]/[second]`
- In this scenario, changing the `second` parameter causes a re-run on the root \_\_layout

travel to route `[first]` and back to `[first]/[second]` (the `first` parameter must not change)
- now changing the `second` parameter won't cause a re-run on the root \_\_layout, because changing to `[first]` made it not track the `second` parameter anymore

### Logs

_No response_

### System Info

```shell
System:
    OS: Windows 10 10.0.19044
    CPU: (8) x64 Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz    
    Memory: 2.83 GB / 11.86 GB
  Binaries:
    Node: 16.14.0 - C:\Program Files\nodejs\node.EXE
    npm: 8.3.1 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.19041.1266.0), Chromium (101.0.1210.47)
    Internet Explorer: 11.0.19041.1566
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.42
    @sveltejs/kit: next => 1.0.0-next.330
    svelte: ^3.44.0 => 3.48.0
```


### Severity

annoyance

### Additional Information

_No response_

Pull request description:
Fix `changed.params` tracking
Fixes #5004. Leaving this here until I have sufficient motivation to tidy it up and add tests

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [ ] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [ ] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index f28b6ef0a..195eab6ca 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -521,22 +521,15 @@ export function create_client({ target, base, trailing_slash }) {
 				}
 			}
 
-			/** @type {Record<string, string>} */
-			const uses_params = {};
-			for (const key in params) {
-				Object.defineProperty(uses_params, key, {
-					get() {
-						uses.params.add(key);
-						return params[key];
-					},
-					enumerable: true
-				});
-			}
-
 			/** @type {import('types').LoadEvent} */
 			const load_input = {
 				routeId,
-				params: uses_params,
+				params: new Proxy(params, {
+					get: (target, key) => {
+						uses.params.add(/** @type {string} */ (key));
+						return target[/** @type {string} */ (key)];
+					}
+				}),
 				data: server_data_node?.data ?? null,
 				url: make_trackable(url, () => {
 					uses.url = true;
@@ -640,20 +633,21 @@ export function create_client({ target, base, trailing_slash }) {
 	}
 
 	/**
-	 * @param {import('types').Uses | undefined} uses
+	 * @param {boolean} url_changed
 	 * @param {boolean} parent_changed
-	 * @param {{ url: boolean, params: string[] }} changed
+	 * @param {import('types').Uses | undefined} uses
+	 * @param {Record<string, string>} params
 	 */
-	function has_changed(changed, parent_changed, uses) {
+	function has_changed(url_changed, parent_changed, uses, params) {
 		if (force_invalidation) return true;
 
 		if (!uses) return false;
 
 		if (uses.parent && parent_changed) return true;
-		if (changed.url && uses.url) return true;
+		if (uses.url && url_changed) return true;
 
-		for (const param of changed.params) {
-			if (uses.params.has(param)) return true;
+		for (const param of uses.params) {
+			if (params[param] !== current.params[param]) return true;
 		}
 
 		for (const href of uses.dependencies) {
@@ -697,11 +691,6 @@ export function create_client({ target, base, trailing_slash }) {
 
 		const { errors, layouts, leaf } = route;
 
-		const changed = current.url && {
-			url: id !== current.url.pathname + current.url.search,
-			params: Object.keys(params).filter((key) => current.params[key] !== params[key])
-		};
-
 		const loaders = [...layouts, leaf];
 
 		// preload modules to avoid waterfall, but handle rejections
@@ -713,12 +702,15 @@ export function create_client({ target, base, trailing_slash }) {
 		/** @type {import('types').ServerData | null} */
 		let server_data = null;
 
+		const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
+
 		const invalid_server_nodes = loaders.reduce((acc, loader, i) => {
 			const previous = current.branch[i];
+
 			const invalid =
 				!!loader?.[0] &&
 				(previous?.loader !== loader[1] ||
-					has_changed(changed, acc.some(Boolean), previous.server?.uses));
+					has_changed(url_changed, acc.some(Boolean), previous.server?.uses, params));
 
 			acc.push(invalid);
 			return acc;
@@ -757,7 +749,7 @@ export function create_client({ target, base, trailing_slash }) {
 			const valid =
 				(!server_data_node || server_data_node.type === 'skip') &&
 				loader[1] === previous?.loader &&
-				!has_changed(changed, parent_changed, previous.shared?.uses);
+				!has_changed(url_changed, parent_changed, previous.shared?.uses, params);
 			if (valid) return previous;
 
 			parent_changed = true;


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.js
new file mode 100644
index 000000000..1d25f6234
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.js
@@ -0,0 +1,7 @@
+/** @type {import('./$types').LayoutLoad} */
+export function load({ params }) {
+	return {
+		a: params.a,
+		b: params.b
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.svelte
new file mode 100644
index 000000000..9b3472274
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.svelte
@@ -0,0 +1,14 @@
+<script>
+	/** @type {import('./$types').LayoutData} */
+	export let data;
+</script>
+
+<slot />
+
+<pre>{JSON.stringify({
+		a: data.a,
+		b: data.b
+	})}</pre>
+
+<a href="/load/invalidation/params/1">1</a>
+<a href="/load/invalidation/params/1/x">1/x</a>
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+page.svelte
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/+page.svelte
new file mode 100644
index 000000000..465276da1
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/+page.svelte
@@ -0,0 +1,1 @@
+<h1>[a]</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/[b]/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/[b]/+page.svelte
new file mode 100644
index 000000000..956ca34c9
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/[b]/+page.svelte
@@ -0,0 +1,1 @@
+<h1>[a]/[b]</h1>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 2612eb7ff..a1a31b5e3 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -810,6 +810,22 @@ test.describe.serial('Invalidation', () => {
 		expect(server).toBe(next_server);
 		expect(shared).not.toBe(next_shared);
 	});
+
+	test('Parameter use is tracked even for routes that do not use the parameters', async ({
+		page,
+		clicknav
+	}) => {
+		await page.goto('/load/invalidation/params');
+
+		await clicknav('[href="/load/invalidation/params/1"]');
+		expect(await page.textContent('pre')).toBe('{"a":"1"}');
+
+		await clicknav('[href="/load/invalidation/params/1/x"]');
+		expect(await page.textContent('pre')).toBe('{"a":"1","b":"x"}');
+
+		await page.goBack();
+		expect(await page.textContent('pre')).toBe('{"a":"1"}');
+	});
 });
 
 test.describe('data-sveltekit attributes', () => {