diff --git a/.changeset/refresh-page-state.md b/.changeset/refresh-page-state.md new file mode 100644 index 000000000000..f69d1876700a --- /dev/null +++ b/.changeset/refresh-page-state.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: add `refreshAll` and deprecate `invalidateAll` diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index 2c895dc6af72..2146de9a56bc 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -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]:`: @@ -661,7 +663,7 @@ export async function load({ fetch, depends }) { ```svelte @@ -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 `` link click, a [`
` interaction](form-actions#GET-vs-POST), a [`goto`]($app-navigation#goto) invocation, or a [`redirect`](@sveltejs-kit#redirect). diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index c8e69846efa5..6642f17ba4ad 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -433,7 +433,7 @@ We can also implement progressive enhancement ourselves, without `use:enhance`, ```svelte @@ -111,5 +111,5 @@

{stream_log}

- -

{invalidate_state}

+ +

{refresh_state}

diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index 208876495c8a..5b47b97a6e62 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -372,20 +372,6 @@ test.describe('remote function mutations', () => { expect(request_count).toBe(5); }); - test('refreshAll({ includeLoadFunctions: false }) reloads remote functions only', async ({ - page - }) => { - await page.goto('/remote'); - await expect(page.locator('#count-result')).toHaveText('0 / 0 (false)'); - - let request_count = 0; - page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0)); - - await page.click('#refresh-remote-only'); - await page.waitForTimeout(100); // allow things to rerun - expect(request_count).toBe(4); - }); - test('command tracks pending state', async ({ page }) => { await page.goto('/remote'); @@ -806,7 +792,7 @@ test.describe('remote function mutations', () => { await page.click('#reset'); }); - test('for await consumers continue receiving values across invalidateAll-triggered reconnects', async ({ + test('for await consumers continue receiving values across refreshAll-triggered reconnects', async ({ page }) => { await page.goto('/remote/live'); @@ -817,11 +803,11 @@ test.describe('remote function mutations', () => { // the first value should be the current value (0) await expect(page.locator('#stream-log')).toHaveText(/0/); - // invalidateAll() calls reconnect(), which keeps the existing fan-out + // refreshAll() calls reconnect(), which keeps the existing fan-out // open so active `for await` consumers continue receiving values from // the new connection without interruption. - await page.click('#run-invalidate-all'); - await expect(page.locator('#invalidate-state')).toHaveText('resolved'); + await page.click('#run-refresh-all'); + await expect(page.locator('#refresh-state')).toHaveText('resolved'); // Trigger a new value — the still-attached consumer must see it. // Before the fix the fan-out was replaced, orphaning the subscriber so @@ -832,7 +818,7 @@ test.describe('remote function mutations', () => { await page.click('#reset'); }); - test('invalidateAll resolves while a live query is offline', async ({ page, context }) => { + test('refreshAll resolves while a live query is offline', async ({ page, context }) => { await page.goto('/remote/live'); await page.click('#reset'); await expect(page.locator('#connected')).toHaveText('true'); @@ -840,9 +826,9 @@ test.describe('remote function mutations', () => { await context.setOffline(true); // reconnect()'s handshake must settle on every #main exit path (here: - // offline) so that awaiting invalidateAll() doesn't deadlock. - await page.click('#run-invalidate-all'); - await expect(page.locator('#invalidate-state')).toHaveText(/resolved|rejected/); + // offline) so that awaiting refreshAll() doesn't deadlock. + await page.click('#run-refresh-all'); + await expect(page.locator('#refresh-state')).toHaveText(/resolved|rejected/); await context.setOffline(false); }); diff --git a/packages/kit/test/apps/basics/src/routes/actions/update-form/+page.svelte b/packages/kit/test/apps/basics/src/routes/actions/update-form/+page.svelte index e42c285b54c8..3cee4d352d6f 100644 --- a/packages/kit/test/apps/basics/src/routes/actions/update-form/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/actions/update-form/+page.svelte @@ -1,6 +1,6 @@ @@ -8,17 +8,17 @@
b setTimeout(() => invalidateAll(), 50)} + data-testid="nav-b-refresh" + onclick={() => setTimeout(() => refreshAll(), 50)} > - b+invalidate + b+refresh setTimeout(() => invalidateAll(), 50)} + data-testid="nav-a-refresh" + onclick={() => setTimeout(() => refreshAll(), 50)} > - a+invalidate + a+refresh diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/forced-goto/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/forced-goto/+page.svelte index 76d828cac677..129a6078e95f 100644 --- a/packages/kit/test/apps/basics/src/routes/load/invalidation/forced-goto/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/forced-goto/+page.svelte @@ -1,5 +1,5 @@ - - - + + +

layout: {page.data.count_layout}, page: {page.data.count_page}

diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.svelte index 141db8bcff59..21d1cc0321d9 100644 --- a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.svelte @@ -1,10 +1,10 @@ diff --git a/packages/kit/test/apps/basics/src/routes/reroute/invalidate/+page.svelte b/packages/kit/test/apps/basics/src/routes/reroute/invalidate/+page.svelte index 77c4945350a1..16cb78956569 100644 --- a/packages/kit/test/apps/basics/src/routes/reroute/invalidate/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/reroute/invalidate/+page.svelte @@ -1,10 +1,10 @@ - + {#if data.request}

data request: {data.request}

diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte index 318d0c91cbd9..07c88d358be5 100644 --- a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte @@ -1,5 +1,5 @@ + +

refresh

+ + + + + + +

active: {page.state.active ?? false}

+{data.now} diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 1e5b36b519f5..c9a069649f35 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -76,7 +76,7 @@ test.describe('Load', () => { expect(await page.textContent('h1')).toBe('layout loads: 5'); expect(await page.textContent('h2')).toBe('x: b: 3'); - await page.click('button:has-text("invalidate all")'); + await page.click('button:has-text("refresh all")'); await page.waitForFunction('window.invalidated'); expect(await page.textContent('h1')).toBe('layout loads: 6'); expect(await page.textContent('h2')).toBe('x: b: 4'); @@ -536,13 +536,13 @@ test.describe('Invalidation', () => { await page.goto('/load/invalidation/forced'); expect(await page.textContent('h1')).toBe('a: 0, b: 0'); - await page.click('button.invalidateall'); + await page.click('button.refreshall'); await page.evaluate( () => /** @type {Window & typeof globalThis & { promise: Promise }} */ (window).promise ); expect(await page.textContent('h1')).toBe('a: 1, b: 1'); - await page.click('button.invalidateall'); + await page.click('button.refreshall'); await page.evaluate( () => /** @type {Window & typeof globalThis & { promise: Promise }} */ (window).promise ); @@ -590,7 +590,7 @@ test.describe('Invalidation', () => { await expect(btn).toHaveText('2'); }); - test('invalidateAll persists through redirects', async ({ page }) => { + test('refreshAll persists through redirects', async ({ page }) => { await page.goto('/load/invalidation/multiple/redirect'); await page.locator('button.redirect').click(); await expect(page.locator('p.redirect-state')).toHaveText('Redirect state: done'); @@ -795,7 +795,7 @@ test.describe('Invalidation', () => { await expect(page.getByText('updated')).toBeVisible(); }); - test('goto after invalidation does not reset state', async ({ page }) => { + test('goto after invalidate does not reset state', async ({ page }) => { await page.goto('/load/invalidation/invalidate-then-goto'); const layout = await page.textContent('p.layout'); const _page = await page.textContent('p.page'); @@ -821,28 +821,28 @@ test.describe('Invalidation', () => { expect(next_page_2).not.toBe(next_page_1); }); - test('invalidateAll finishing after navigation does not apply stale data', async ({ + test('refreshAll finishing after navigation does not apply stale data', async ({ page, clicknav }) => { await page.goto('/load/invalidation/during-navigation/a'); await expect(page.locator('[data-testid="scores"]')).toHaveText('1 - 1'); - await clicknav('[data-testid="nav-b-invalidate"]'); + await clicknav('[data-testid="nav-b-refresh"]'); await expect(page.locator('[data-testid="scores"]')).toHaveText('2 - 2'); await page.waitForTimeout(400); await expect(page.locator('[data-testid="scores"]')).toHaveText('2 - 2'); }); - test('invalidateAll finishing before navigation ends does not prevent navigation', async ({ + test('refreshAll finishing before navigation ends does not prevent navigation', async ({ page, clicknav }) => { await page.goto('/load/invalidation/during-navigation/b'); await expect(page.locator('[data-testid="scores"]')).toHaveText('2 - 2'); - await clicknav('[data-testid="nav-a-invalidate"]'); + await clicknav('[data-testid="nav-a-refresh"]'); await expect(page.locator('[data-testid="scores"]')).toHaveText('1 - 1'); }); }); @@ -1679,7 +1679,7 @@ test.describe('Shallow routing', () => { await page.locator('[data-id="two"]').click(); expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state/a`); - await page.locator('[data-id="invalidate"]').click(); + await page.locator('[data-id="refresh"]').click(); await expect(page.locator('h1')).toHaveText('parent'); await expect(page.locator('span')).not.toHaveText(now); }); @@ -1742,6 +1742,51 @@ test.describe('Shallow routing', () => { await page.locator('button').click(); await expect(page.locator('p')).toHaveText('count: 1'); }); + + test('refreshAll reruns load functions without resetting page.state', async ({ page }) => { + await page.goto('/shallow-routing/refresh'); + await expect(page.locator('p')).toHaveText('active: false'); + + const now = /** @type {string} */ (await page.locator('span').textContent()); + + await page.locator('[data-id="activate"]').click(); + await expect(page.locator('p')).toHaveText('active: true'); + + await page.locator('[data-id="refreshAll"]').click(); + await page.evaluate(() => window.promise); + await expect(page.locator('p')).toHaveText('active: true'); + await expect(page.locator('span')).not.toHaveText(now); + }); + + test('invalidate resets page.state', async ({ page }) => { + await page.goto('/shallow-routing/refresh'); + await expect(page.locator('p')).toHaveText('active: false'); + + const now = /** @type {string} */ (await page.locator('span').textContent()); + + await page.locator('[data-id="activate"]').click(); + await expect(page.locator('p')).toHaveText('active: true'); + + await page.locator('[data-id="invalidate"]').click(); + await page.evaluate(() => window.promise); + await expect(page.locator('p')).toHaveText('active: false'); + await expect(page.locator('span')).not.toHaveText(now); + }); + + test('invalidateAll resets page.state', async ({ page }) => { + await page.goto('/shallow-routing/refresh'); + await expect(page.locator('p')).toHaveText('active: false'); + + const now = /** @type {string} */ (await page.locator('span').textContent()); + + await page.locator('[data-id="activate"]').click(); + await expect(page.locator('p')).toHaveText('active: true'); + + await page.locator('[data-id="invalidateAll"]').click(); + await page.evaluate(() => window.promise); + await expect(page.locator('p')).toHaveText('active: false'); + await expect(page.locator('span')).not.toHaveText(now); + }); }); test.describe('reroute', () => { diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 25ffbe4ced36..b6dac3ac1f07 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1078,7 +1078,7 @@ test.describe('Actions', () => { } }); - test('form prop stays after invalidation and is reset on navigation', async ({ + test('form prop stays after refresh and is reset on navigation', async ({ page, app, javaScriptEnabled @@ -1090,7 +1090,7 @@ test.describe('Actions', () => { await page.locator('button.increment-success').click(); await expect(page.locator('pre')).toHaveText(JSON.stringify({ count: 0 })); - await page.locator('button.invalidateAll').click(); + await page.locator('button.refreshAll').click(); await page.waitForTimeout(500); await expect(page.locator('pre')).toHaveText(JSON.stringify({ count: 0 })); await app.goto('/actions/enhance'); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 318380ee4e5a..1ac57eb7a3af 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3361,8 +3361,9 @@ declare module '$app/navigation' { replaceState?: boolean | undefined; noScroll?: boolean | undefined; keepFocus?: boolean | undefined; - invalidateAll?: boolean | undefined; + refreshAll?: boolean | undefined; invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; + invalidateAll?: boolean | undefined; state?: App.PageState | undefined; }): Promise; /** @@ -3381,19 +3382,22 @@ declare module '$app/navigation' { * invalidate((url) => url.pathname === '/path'); * ``` * @param resource The invalidated URL + * @param keepState If `true`, the current `page.state` will be preserved. Otherwise, it will be reset to an empty object. `false` by default. * */ - export function invalidate(resource: string | URL | ((url: URL) => boolean)): Promise; + export function invalidate(resource: string | URL | ((url: URL) => boolean), keepState?: boolean): Promise; /** * 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`. * */ export function invalidateAll(): Promise; /** - * 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. * */ - export function refreshAll({ includeLoadFunctions }?: { - includeLoadFunctions?: boolean; - }): Promise; + export function refreshAll(): Promise; /** * Programmatically preloads the given page, which means * 1. ensuring that the code for the page is loaded, and