Skip to content

Commit 5c89490

Browse files
authored
feat: add untrack to load functions (#11311)
closes #6294
1 parent 052adf9 commit 5c89490

File tree

13 files changed

+205
-14
lines changed

13 files changed

+205
-14
lines changed

.changeset/fast-eyes-deny.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sveltejs/kit": minor
3+
---
4+
5+
feat: add untrack to load

documentation/docs/20-core-concepts/20-load.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ A `load` function is invoked at runtime, unless you [prerender](page-options#pre
172172

173173
### Input
174174

175-
Both universal and server `load` functions have access to properties describing the request (`params`, `route` and `url`) and various functions (`fetch`, `setHeaders`, `parent` and `depends`). These are described in the following sections.
175+
Both universal and server `load` functions have access to properties describing the request (`params`, `route` and `url`) and various functions (`fetch`, `setHeaders`, `parent`, `depends` and `untrack`). These are described in the following sections.
176176

177177
Server `load` functions are called with a `ServerLoadEvent`, which inherits `clientAddress`, `cookies`, `locals`, `platform` and `request` from `RequestEvent`.
178178

@@ -574,6 +574,21 @@ Dependency tracking does not apply _after_ the `load` function has returned —
574574

575575
Search parameters are tracked independently from the rest of the url. For example, accessing `event.url.searchParams.get("x")` inside a `load` function will make that `load` function re-run when navigating from `?x=1` to `?x=2`, but not when navigating from `?x=1&y=1` to `?x=1&y=2`.
576576

577+
### Untracking dependencies
578+
579+
In rare cases, you may wish to exclude something from the dependency tracking mechanism. You can do this with the provided `untrack` function:
580+
581+
```js
582+
/// file: src/routes/+page.js
583+
/** @type {import('./$types').PageLoad} */
584+
export async function load({ untrack, url }) {
585+
// Untrack url.pathname so that path changes don't trigger a rerun
586+
if (untrack(() => url.pathname === '/')) {
587+
return { message: 'Welcome!' };
588+
}
589+
}
590+
```
591+
577592
### Manual invalidation
578593

579594
You can also rerun `load` functions that apply to the current page using [`invalidate(url)`](modules#$app-navigation-invalidate), which reruns all `load` functions that depend on `url`, and [`invalidateAll()`](modules#$app-navigation-invalidateall), which reruns every `load` function. Server load functions will never automatically depend on a fetched `url` to avoid leaking secrets to the client.

packages/kit/src/exports/public.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,20 @@ export interface LoadEvent<
791791
* ```
792792
*/
793793
depends(...deps: Array<`${string}:${string}`>): void;
794+
/**
795+
* Use this function to opt out of dependency tracking for everything that is synchronously called within the callback. Example:
796+
*
797+
* ```js
798+
* /// file: src/routes/+page.server.js
799+
* export async function load({ untrack, url }) {
800+
* // Untrack url.pathname so that path changes don't trigger a rerun
801+
* if (untrack(() => url.pathname === '/')) {
802+
* return { message: 'Welcome!' };
803+
* }
804+
* }
805+
* ```
806+
*/
807+
untrack<T>(fn: () => T): T;
794808
}
795809

796810
export interface NavigationEvent<
@@ -1196,6 +1210,20 @@ export interface ServerLoadEvent<
11961210
* ```
11971211
*/
11981212
depends(...deps: string[]): void;
1213+
/**
1214+
* Use this function to opt out of dependency tracking for everything that is synchronously called within the callback. Example:
1215+
*
1216+
* ```js
1217+
* /// file: src/routes/+page.js
1218+
* export async function load({ untrack, url }) {
1219+
* // Untrack url.pathname so that path changes don't trigger a rerun
1220+
* if (untrack(() => url.pathname === '/')) {
1221+
* return { message: 'Welcome!' };
1222+
* }
1223+
* }
1224+
* ```
1225+
*/
1226+
untrack<T>(fn: () => T): T;
11991227
}
12001228

12011229
/**

packages/kit/src/runtime/client/client.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,8 @@ export function create_client(app, target) {
501501
/** @type {Record<string, any> | null} */
502502
let data = null;
503503

504+
let is_tracking = true;
505+
504506
/** @type {import('types').Uses} */
505507
const uses = {
506508
dependencies: new Set(),
@@ -532,21 +534,33 @@ export function create_client(app, target) {
532534
const load_input = {
533535
route: new Proxy(route, {
534536
get: (target, key) => {
535-
uses.route = true;
537+
if (is_tracking) {
538+
uses.route = true;
539+
}
536540
return target[/** @type {'id'} */ (key)];
537541
}
538542
}),
539543
params: new Proxy(params, {
540544
get: (target, key) => {
541-
uses.params.add(/** @type {string} */ (key));
545+
if (is_tracking) {
546+
uses.params.add(/** @type {string} */ (key));
547+
}
542548
return target[/** @type {string} */ (key)];
543549
}
544550
}),
545551
data: server_data_node?.data ?? null,
546552
url: make_trackable(
547553
url,
548-
() => (uses.url = true),
549-
(param) => uses.search_params.add(param)
554+
() => {
555+
if (is_tracking) {
556+
uses.url = true;
557+
}
558+
},
559+
(param) => {
560+
if (is_tracking) {
561+
uses.search_params.add(param);
562+
}
563+
}
550564
),
551565
async fetch(resource, init) {
552566
/** @type {URL | string} */
@@ -583,7 +597,9 @@ export function create_client(app, target) {
583597

584598
// we must fixup relative urls so they are resolved from the target page
585599
const resolved = new URL(requested, url);
586-
depends(resolved.href);
600+
if (is_tracking) {
601+
depends(resolved.href);
602+
}
587603

588604
// match ssr serialized data url, which is important to find cached responses
589605
if (resolved.origin === url.origin) {
@@ -598,8 +614,18 @@ export function create_client(app, target) {
598614
setHeaders: () => {}, // noop
599615
depends,
600616
parent() {
601-
uses.parent = true;
617+
if (is_tracking) {
618+
uses.parent = true;
619+
}
602620
return parent();
621+
},
622+
untrack(fn) {
623+
is_tracking = false;
624+
try {
625+
return fn();
626+
} finally {
627+
is_tracking = true;
628+
}
603629
}
604630
};
605631

packages/kit/src/runtime/server/page/load_data.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export async function load_server_data({ event, state, node, parent }) {
1616
if (!node?.server) return null;
1717

1818
let done = false;
19+
let is_tracking = true;
1920

2021
const uses = {
2122
dependencies: new Set(),
@@ -35,7 +36,9 @@ export async function load_server_data({ event, state, node, parent }) {
3536
);
3637
}
3738

38-
uses.url = true;
39+
if (is_tracking) {
40+
uses.url = true;
41+
}
3942
},
4043
(param) => {
4144
if (DEV && done && !uses.search_params.has(param)) {
@@ -44,7 +47,9 @@ export async function load_server_data({ event, state, node, parent }) {
4447
);
4548
}
4649

47-
uses.search_params.add(param);
50+
if (is_tracking) {
51+
uses.search_params.add(param);
52+
}
4853
}
4954
);
5055

@@ -63,6 +68,7 @@ export async function load_server_data({ event, state, node, parent }) {
6368
);
6469
}
6570

71+
// Note: server fetches are not added to uses.depends due to security concerns
6672
return event.fetch(info, init);
6773
},
6874
/** @param {string[]} deps */
@@ -93,7 +99,9 @@ export async function load_server_data({ event, state, node, parent }) {
9399
);
94100
}
95101

96-
uses.params.add(key);
102+
if (is_tracking) {
103+
uses.params.add(key);
104+
}
97105
return target[/** @type {string} */ (key)];
98106
}
99107
}),
@@ -104,7 +112,9 @@ export async function load_server_data({ event, state, node, parent }) {
104112
);
105113
}
106114

107-
uses.parent = true;
115+
if (is_tracking) {
116+
uses.parent = true;
117+
}
108118
return parent();
109119
},
110120
route: new Proxy(event.route, {
@@ -117,11 +127,21 @@ export async function load_server_data({ event, state, node, parent }) {
117127
);
118128
}
119129

120-
uses.route = true;
130+
if (is_tracking) {
131+
uses.route = true;
132+
}
121133
return target[/** @type {'id'} */ (key)];
122134
}
123135
}),
124-
url
136+
url,
137+
untrack(fn) {
138+
is_tracking = false;
139+
try {
140+
return fn();
141+
} finally {
142+
is_tracking = true;
143+
}
144+
}
125145
});
126146

127147
if (__SVELTEKIT_DEV__) {
@@ -176,7 +196,8 @@ export async function load_data({
176196
fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts),
177197
setHeaders: event.setHeaders,
178198
depends: () => {},
179-
parent
199+
parent,
200+
untrack: (fn) => fn()
180201
});
181202

182203
if (__SVELTEKIT_DEV__) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function load({ url }) {
2+
return {
3+
url: url.pathname
4+
};
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function load({ params, parent, url, untrack }) {
2+
untrack(() => {
3+
params.x;
4+
parent();
5+
url.pathname;
6+
url.search;
7+
});
8+
9+
return {
10+
id: Math.random()
11+
};
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
export let data;
3+
</script>
4+
5+
<p class="url">{data.url}</p>
6+
<p class="id">{data.id}</p>
7+
<a href="/untrack/server/2">2</a>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function load({ url }) {
2+
return {
3+
url: url.pathname
4+
};
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function load({ params, parent, url, untrack }) {
2+
untrack(() => {
3+
params.x;
4+
parent();
5+
url.pathname;
6+
url.search;
7+
});
8+
9+
return {
10+
id: Math.random()
11+
};
12+
}

0 commit comments

Comments
 (0)