Skip to content

Commit

Permalink
feat: add untrack to load functions (#11311)
Browse files Browse the repository at this point in the history
closes #6294
  • Loading branch information
dummdidumm authored Dec 14, 2023
1 parent 052adf9 commit 5c89490
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-eyes-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sveltejs/kit": minor
---

feat: add untrack to load
17 changes: 16 additions & 1 deletion documentation/docs/20-core-concepts/20-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ A `load` function is invoked at runtime, unless you [prerender](page-options#pre

### Input

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

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

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

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`.

### Untracking dependencies

In rare cases, you may wish to exclude something from the dependency tracking mechanism. You can do this with the provided `untrack` function:

```js
/// file: src/routes/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ untrack, url }) {
// Untrack url.pathname so that path changes don't trigger a rerun
if (untrack(() => url.pathname === '/')) {
return { message: 'Welcome!' };
}
}
```

### Manual invalidation

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.
Expand Down
28 changes: 28 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,20 @@ export interface LoadEvent<
* ```
*/
depends(...deps: Array<`${string}:${string}`>): void;
/**
* Use this function to opt out of dependency tracking for everything that is synchronously called within the callback. Example:
*
* ```js
* /// file: src/routes/+page.server.js
* export async function load({ untrack, url }) {
* // Untrack url.pathname so that path changes don't trigger a rerun
* if (untrack(() => url.pathname === '/')) {
* return { message: 'Welcome!' };
* }
* }
* ```
*/
untrack<T>(fn: () => T): T;
}

export interface NavigationEvent<
Expand Down Expand Up @@ -1196,6 +1210,20 @@ export interface ServerLoadEvent<
* ```
*/
depends(...deps: string[]): void;
/**
* Use this function to opt out of dependency tracking for everything that is synchronously called within the callback. Example:
*
* ```js
* /// file: src/routes/+page.js
* export async function load({ untrack, url }) {
* // Untrack url.pathname so that path changes don't trigger a rerun
* if (untrack(() => url.pathname === '/')) {
* return { message: 'Welcome!' };
* }
* }
* ```
*/
untrack<T>(fn: () => T): T;
}

/**
Expand Down
38 changes: 32 additions & 6 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ export function create_client(app, target) {
/** @type {Record<string, any> | null} */
let data = null;

let is_tracking = true;

/** @type {import('types').Uses} */
const uses = {
dependencies: new Set(),
Expand Down Expand Up @@ -532,21 +534,33 @@ export function create_client(app, target) {
const load_input = {
route: new Proxy(route, {
get: (target, key) => {
uses.route = true;
if (is_tracking) {
uses.route = true;
}
return target[/** @type {'id'} */ (key)];
}
}),
params: new Proxy(params, {
get: (target, key) => {
uses.params.add(/** @type {string} */ (key));
if (is_tracking) {
uses.params.add(/** @type {string} */ (key));
}
return target[/** @type {string} */ (key)];
}
}),
data: server_data_node?.data ?? null,
url: make_trackable(
url,
() => (uses.url = true),
(param) => uses.search_params.add(param)
() => {
if (is_tracking) {
uses.url = true;
}
},
(param) => {
if (is_tracking) {
uses.search_params.add(param);
}
}
),
async fetch(resource, init) {
/** @type {URL | string} */
Expand Down Expand Up @@ -583,7 +597,9 @@ export function create_client(app, target) {

// we must fixup relative urls so they are resolved from the target page
const resolved = new URL(requested, url);
depends(resolved.href);
if (is_tracking) {
depends(resolved.href);
}

// match ssr serialized data url, which is important to find cached responses
if (resolved.origin === url.origin) {
Expand All @@ -598,8 +614,18 @@ export function create_client(app, target) {
setHeaders: () => {}, // noop
depends,
parent() {
uses.parent = true;
if (is_tracking) {
uses.parent = true;
}
return parent();
},
untrack(fn) {
is_tracking = false;
try {
return fn();
} finally {
is_tracking = true;
}
}
};

Expand Down
35 changes: 28 additions & 7 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export async function load_server_data({ event, state, node, parent }) {
if (!node?.server) return null;

let done = false;
let is_tracking = true;

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

uses.url = true;
if (is_tracking) {
uses.url = true;
}
},
(param) => {
if (DEV && done && !uses.search_params.has(param)) {
Expand All @@ -44,7 +47,9 @@ export async function load_server_data({ event, state, node, parent }) {
);
}

uses.search_params.add(param);
if (is_tracking) {
uses.search_params.add(param);
}
}
);

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

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

uses.params.add(key);
if (is_tracking) {
uses.params.add(key);
}
return target[/** @type {string} */ (key)];
}
}),
Expand All @@ -104,7 +112,9 @@ export async function load_server_data({ event, state, node, parent }) {
);
}

uses.parent = true;
if (is_tracking) {
uses.parent = true;
}
return parent();
},
route: new Proxy(event.route, {
Expand All @@ -117,11 +127,21 @@ export async function load_server_data({ event, state, node, parent }) {
);
}

uses.route = true;
if (is_tracking) {
uses.route = true;
}
return target[/** @type {'id'} */ (key)];
}
}),
url
url,
untrack(fn) {
is_tracking = false;
try {
return fn();
} finally {
is_tracking = true;
}
}
});

if (__SVELTEKIT_DEV__) {
Expand Down Expand Up @@ -176,7 +196,8 @@ export async function load_data({
fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts),
setHeaders: event.setHeaders,
depends: () => {},
parent
parent,
untrack: (fn) => fn()
});

if (__SVELTEKIT_DEV__) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function load({ url }) {
return {
url: url.pathname
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function load({ params, parent, url, untrack }) {
untrack(() => {
params.x;
parent();
url.pathname;
url.search;
});

return {
id: Math.random()
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
export let data;
</script>

<p class="url">{data.url}</p>
<p class="id">{data.id}</p>
<a href="/untrack/server/2">2</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function load({ url }) {
return {
url: url.pathname
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function load({ params, parent, url, untrack }) {
untrack(() => {
params.x;
parent();
url.pathname;
url.search;
});

return {
id: Math.random()
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
export let data;
</script>

<p class="url">{data.url}</p>
<p class="id">{data.id}</p>
<a href="/untrack/universal/2">2</a>
20 changes: 20 additions & 0 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,26 @@ test.describe('goto', () => {
});
});

test.describe('untrack', () => {
test('untracks server load function', async ({ page }) => {
await page.goto('/untrack/server/1');
expect(await page.textContent('p.url')).toBe('/untrack/server/1');
const id = await page.textContent('p.id');
await page.click('a[href="/untrack/server/2"]');
expect(await page.textContent('p.url')).toBe('/untrack/server/2');
expect(await page.textContent('p.id')).toBe(id);
});

test('untracks universal load function', async ({ page }) => {
await page.goto('/untrack/universal/1');
expect(await page.textContent('p.url')).toBe('/untrack/universal/1');
const id = await page.textContent('p.id');
await page.click('a[href="/untrack/universal/2"]');
expect(await page.textContent('p.url')).toBe('/untrack/universal/2');
expect(await page.textContent('p.id')).toBe(id);
});
});

test.describe('Shallow routing', () => {
test('Pushes state to the current URL', async ({ page }) => {
await page.goto('/shallow-routing/push-state');
Expand Down
28 changes: 28 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,20 @@ declare module '@sveltejs/kit' {
* ```
*/
depends(...deps: Array<`${string}:${string}`>): void;
/**
* Use this function to opt out of dependency tracking for everything that is synchronously called within the callback. Example:
*
* ```js
* /// file: src/routes/+page.server.js
* export async function load({ untrack, url }) {
* // Untrack url.pathname so that path changes don't trigger a rerun
* if (untrack(() => url.pathname === '/')) {
* return { message: 'Welcome!' };
* }
* }
* ```
*/
untrack<T>(fn: () => T): T;
}

export interface NavigationEvent<
Expand Down Expand Up @@ -1178,6 +1192,20 @@ declare module '@sveltejs/kit' {
* ```
*/
depends(...deps: string[]): void;
/**
* Use this function to opt out of dependency tracking for everything that is synchronously called within the callback. Example:
*
* ```js
* /// file: src/routes/+page.js
* export async function load({ untrack, url }) {
* // Untrack url.pathname so that path changes don't trigger a rerun
* if (untrack(() => url.pathname === '/')) {
* return { message: 'Welcome!' };
* }
* }
* ```
*/
untrack<T>(fn: () => T): T;
}

/**
Expand Down

0 comments on commit 5c89490

Please sign in to comment.