Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/refresh-page-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: add `refreshAll` and deprecate `invalidateAll`
10 changes: 6 additions & 4 deletions documentation/docs/20-core-concepts/20-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,9 @@ export async function load({ untrack, url }) {

### Manual invalidation

You can also rerun `load` functions that apply to the current page using [`invalidate(url)`]($app-navigation#invalidate), which reruns all `load` functions that depend on `url`, and [`invalidateAll()`]($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.
You can also rerun `load` functions that apply to the current page using [`invalidate(url)`]($app-navigation#invalidate), which reruns all `load` functions that depend on `url`, and [`refreshAll()`]($app-navigation#refreshAll), which reruns every `load` function and all active queries. Server load functions will never automatically depend on a fetched `url` to avoid leaking secrets to the client.

> [!NOTE] `refreshAll` does _not_ reset `page.state`, unlike its deprecated predecessor `invalidateAll`.

A `load` function depends on `url` if it calls `fetch(url)` or `depends(url)`. Note that `url` can be a custom identifier that starts with `[a-z]:`:

Expand All @@ -661,7 +663,7 @@ export async function load({ fetch, depends }) {
```svelte
<!--- file: src/routes/random-number/+page.svelte --->
<script>
import { invalidate, invalidateAll } from '$app/navigation';
import { invalidate, refreshAll } from '$app/navigation';

/** @type {import('./$types').PageProps} */
let { data } = $props();
Expand All @@ -671,7 +673,7 @@ export async function load({ fetch, depends }) {
invalidate('app:random');
invalidate('https://api.example.com/random-number');
invalidate(url => url.href.includes('random-number'));
invalidateAll();
refreshAll();
}
</script>

Expand All @@ -689,7 +691,7 @@ To summarize, a `load` function will rerun in the following situations:
- It calls `await parent()` and a parent `load` function reran
- A child `load` function calls `await parent()` and is rerunning, and the parent is a server load function
- It declared a dependency on a specific URL via [`fetch`](#Making-fetch-requests) (universal load only) or [`depends`](@sveltejs-kit#LoadEvent), and that URL was marked invalid with [`invalidate(url)`]($app-navigation#invalidate)
- All active `load` functions were forcibly rerun with [`invalidateAll()`]($app-navigation#invalidateAll)
- All active `load` functions were forcibly rerun with [`refreshAll()`]($app-navigation#refreshAll)

`params` and `url` can change in response to a `<a href="..">` link click, a [`<form>` interaction](form-actions#GET-vs-POST), a [`goto`]($app-navigation#goto) invocation, or a [`redirect`](@sveltejs-kit#redirect).

Expand Down
6 changes: 3 additions & 3 deletions documentation/docs/20-core-concepts/30-form-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ We can also implement progressive enhancement ourselves, without `use:enhance`,
```svelte
<!--- file: src/routes/login/+page.svelte --->
<script>
import { invalidateAll, goto } from '$app/navigation';
import { refreshAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';

/** @type {import('./$types').PageProps} */
Expand All @@ -453,8 +453,8 @@ We can also implement progressive enhancement ourselves, without `use:enhance`,
const result = deserialize(await response.text());

if (result.type === 'success') {
// rerun all `load` functions, following the successful update
await invalidateAll();
// rerun all `load` functions and queries, following the successful update
await refreshAll();
}

applyAction(result);
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/app/forms.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as devalue from 'devalue';
import { BROWSER, DEV } from 'esm-env';
import { noop } from '../../utils/functions.js';
import { invalidateAll } from './navigation.js';
import { refreshAll } from './navigation.js';
import { app as client_app, applyAction, handle_error } from '../client/client.js';
import { app as server_app } from '../server/app.js';

Expand Down Expand Up @@ -106,7 +106,7 @@ export function enhance(form_element, submit = noop) {
HTMLFormElement.prototype.reset.call(form_element);
}
if (shouldInvalidateAll) {
await invalidateAll();
await refreshAll();
}
}

Expand Down
97 changes: 54 additions & 43 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ export async function start(_app, _target, hydrate) {
_start_router();
}

async function _invalidate(include_load_functions = true, reset_page_state = true) {
async function _invalidate(reset_page_state = true) {
// Accept all invalidations as they come, don't swallow any while another invalidation
// is running because subsequent invalidations may make earlier ones outdated,
// but batch multiple synchronous invalidations.
Expand Down Expand Up @@ -442,39 +442,35 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
}
}

if (include_load_functions) {
const prev_state = page.state;
const navigation_result = intent && (await load_route(intent));
if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
return;
}
const prev_state = page.state;
const navigation_result = intent && (await load_route(intent));
if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
return;
}

if (navigation_result.type === 'redirect') {
return _goto(
new URL(navigation_result.location, current.url).href,
{ replaceState: true },
1,
token
);
}
if (navigation_result.type === 'redirect') {
return _goto(
new URL(navigation_result.location, current.url).href,
{ replaceState: true },
1,
token
);
}

// A navigation started before the invalidation and ended before it finished. The invalidation did not redirect,
// hence it likely contains outdated data now, so we ignore it.
if (navigating && !is_navigating) {
return;
}
// A navigation started before the invalidation and ended before it finished. The invalidation did not redirect,
// hence it likely contains outdated data now, so we ignore it.
if (navigating && !is_navigating) {
return;
}

// This is a bit hacky but allows us not having to pass that boolean around, making things harder to reason about
if (!reset_page_state) {
navigation_result.props.page.state = prev_state;
}
update(navigation_result.props.page);
current = { ...navigation_result.state, nav: current.nav };
reset_invalidation();
root.$set(navigation_result.props);
} else {
reset_invalidation();
// Preserve `page.state` when invalidating without resetting it (e.g. `refresh`/`refreshAll`)
if (!reset_page_state) {
navigation_result.props.page.state = prev_state;
}
update(navigation_result.props.page);
current = { ...navigation_result.state, nav: current.nav };
reset_invalidation();
root.$set(navigation_result.props);

// only wait for promises that are connected to queries that still exist
/** @type {Promise<any>[]} */
Expand Down Expand Up @@ -527,7 +523,7 @@ function persist_state() {

/**
* @param {string | URL} url
* @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} options
* @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} options
* @param {number} redirect_count
* @param {{}} [nav_token]
* @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
Expand All @@ -539,9 +535,9 @@ export async function _goto(url, options, redirect_count, nav_token, intent) {
/** @type {Set<string>} */
let live_query_keys;

// Clear preload cache when invalidateAll is true to ensure fresh data
// Clear preload cache when refreshAll is true to ensure fresh data
// after form submissions or explicit invalidations
if (options.invalidateAll) {
if (options.refreshAll) {
discard_load_cache();
}

Expand All @@ -556,7 +552,7 @@ export async function _goto(url, options, redirect_count, nav_token, intent) {
nav_token,
intent,
accept: () => {
if (options.invalidateAll) {
if (options.refreshAll) {
force_invalidation = true;
query_keys = new Set();
for (const [id, entries] of query_map) {
Expand All @@ -582,7 +578,7 @@ export async function _goto(url, options, redirect_count, nav_token, intent) {
}
});

if (options.invalidateAll) {
if (options.refreshAll) {
// TODO the ticks shouldn't be necessary, something inside Svelte itself is buggy
// when a query in a layout that still exists after page change is refreshed earlier than this
void svelte
Expand Down Expand Up @@ -2297,6 +2293,8 @@ export function disableScrollHandling() {
}
}

let warned_on_invalidate_all = false;

/**
* Allows you to navigate programmatically to a given route, with options such as keeping the current element focused.
* Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`.
Expand All @@ -2310,8 +2308,9 @@ export function disableScrollHandling() {
* @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
* @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation
* @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body
* @param {boolean} [opts.invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://svelte.dev/docs/kit/load#rerunning-load-functions for more info on invalidation.
* @param {boolean} [opts.refreshAll] If `true`, all `load` functions and queries of the page will be rerun. See https://svelte.dev/docs/kit/load#rerunning-load-functions for more info on invalidation.
* @param {Array<string | URL | ((url: URL) => boolean)>} [opts.invalidate] Causes any load functions to re-run if they depend on one of the urls
* @param {boolean} [opts.invalidateAll] Deprecated in favour of opts.refreshAll.
* @param {App.PageState} [opts.state] An optional object that will be available as `page.state`
* @returns {Promise<void>}
*/
Expand Down Expand Up @@ -2340,6 +2339,14 @@ export async function goto(url, opts = {}) {
);
}

if (DEV && 'invalidateAll' in opts && !warned_on_invalidate_all) {
warned_on_invalidate_all = true;
console.warn(
`The \`goto(..., { invalidateAll: ${opts.invalidateAll} })\` option has been deprecated in favour of \`refreshAll\``
);
}

opts.refreshAll = opts.refreshAll ?? opts.invalidateAll;
Comment thread
Rich-Harris marked this conversation as resolved.
return _goto(url, opts, 0, {}, intent);
}

Expand All @@ -2359,16 +2366,17 @@ export async function goto(url, opts = {}) {
* invalidate((url) => url.pathname === '/path');
* ```
* @param {string | URL | ((url: URL) => boolean)} resource The invalidated URL
* @param {boolean} [keepState] If `true`, the current `page.state` will be preserved. Otherwise, it will be reset to an empty object. `false` by default.
* @returns {Promise<void>}
*/
export function invalidate(resource) {
export function invalidate(resource, keepState = false) {
if (!BROWSER) {
throw new Error('Cannot call invalidate(...) on the server');
}

push_invalidated(resource);

return _invalidate();
return _invalidate(!keepState);
}

/**
Expand All @@ -2385,6 +2393,10 @@ function push_invalidated(resource) {

/**
* Causes all `load` and `query` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated.
*
* Note that this resets `page.state` to an empty object. If you want to preserve `page.state` (for example when using [shallow routing](https://svelte.dev/docs/kit/shallow-routing)), use `refreshAll` instead.
*
* @deprecated Use [`refreshAll`](https://svelte.dev/docs/kit/$app-navigation#refreshAll) instead. Unlike `invalidateAll`, `refreshAll` does not reset `page.state`.
* @returns {Promise<void>}
*/
export function invalidateAll() {
Expand All @@ -2397,18 +2409,17 @@ export function invalidateAll() {
}

/**
* Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run (unless disabled via the option argument).
* Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run.
* Returns a `Promise` that resolves when the page is subsequently updated.
* @param {{ includeLoadFunctions?: boolean }} [options]
* @returns {Promise<void>}
*/
export function refreshAll({ includeLoadFunctions = true } = {}) {
export function refreshAll() {
if (!BROWSER) {
throw new Error('Cannot call refreshAll() on the server');
}

force_invalidation = true;
return _invalidate(includeLoadFunctions, false);
return _invalidate(false);
}

/**
Expand Down Expand Up @@ -2599,7 +2610,7 @@ export async function applyAction(result) {
if (result.type === 'error') {
await set_nearest_error_page(result.error);
} else if (result.type === 'redirect') {
await _goto(result.location, { invalidateAll: true }, 0);
await _goto(result.location, { refreshAll: true }, 0);
} else {
page.form = result.data;
page.status = result.status;
Expand Down
12 changes: 6 additions & 6 deletions packages/kit/src/runtime/client/remote-functions/form.svelte.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
query_responses,
_goto,
set_nearest_error_page,
invalidateAll,
handle_error
handle_error,
refreshAll
} from '../client.js';
import { tick } from 'svelte';
import { categorize_updates, remote_request } from './shared.svelte.js';
Expand Down Expand Up @@ -227,14 +227,14 @@ export function form(id) {

// if the developer took control of updates via `.updates(...)` (even with
// no arguments), or the server performed explicit refreshes, don't invalidateAll
const should_invalidate = refreshes === null && !response.r;
const should_refresh = refreshes === null && !response.r;

if (response.redirect) {
// Use internal version to allow redirects to external URLs
void _goto(
response.redirect,
{
invalidateAll: should_invalidate
refreshAll: should_refresh
},
0
);
Expand All @@ -244,8 +244,8 @@ export function form(id) {
const succeeded = raw_issues.length === 0;

if (succeeded) {
if (should_invalidate) {
void invalidateAll();
if (should_refresh) {
void refreshAll();
}
} else {
if (DEV) {
Expand Down
3 changes: 0 additions & 3 deletions packages/kit/test/apps/async/src/routes/remote/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,6 @@
</button>

<button id="refresh-all" onclick={() => refreshAll()}>refreshAll</button>
<button id="refresh-remote-only" onclick={() => refreshAll({ includeLoadFunctions: false })}>
refreshAll (remote functions only)
</button>
<button id="resolve-deferreds" onclick={() => resolve_deferreds()}>Resolve Deferreds</button>

<a href="/remote/event">/remote/event</a>
18 changes: 9 additions & 9 deletions packages/kit/test/apps/async/src/routes/remote/live/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { refreshAll } from '$app/navigation';
import LiveView from './LiveView.svelte';
import {
increment,
Expand Down Expand Up @@ -67,15 +67,15 @@
}
}

let invalidate_state = $state('idle');
let refresh_state = $state('idle');

async function run_invalidate_all() {
invalidate_state = 'pending';
async function run_refresh_all() {
refresh_state = 'pending';
try {
await invalidateAll();
invalidate_state = 'resolved';
await refreshAll();
refresh_state = 'resolved';
} catch {
invalidate_state = 'rejected';
refresh_state = 'rejected';
}
}
</script>
Expand Down Expand Up @@ -111,5 +111,5 @@

<button id="start-stream-log" onclick={start_stream_log}>start stream log</button>
<p id="stream-log">{stream_log}</p>
<button id="run-invalidate-all" onclick={run_invalidate_all}>invalidate all</button>
<p id="invalidate-state">{invalidate_state}</p>
<button id="run-refresh-all" onclick={run_refresh_all}>refresh all</button>
<p id="refresh-state">{refresh_state}</p>
Loading
Loading