Summary
Once a page is rendered, there is no supported way to re-run its <script server> block. The router's public API has no refresh, and navigating to the URL you are already on early-returns. The force argument that would bypass that early return is a third positional parameter on the router's internal navigate, and the runtime's exported navigate never passes it. So the only way for an app to reflect a server-side mutation is { reload: true } — a deliberate full document load that discards every client signal on the page.
There is also no partial equivalent: no way to re-fetch just one section of the server data.
Impact (real app)
analyticshq's dashboard runs ~25 SQL queries in its server block (resources/views/dashboard.stx:337-503). Adding or deleting one conversion goal re-runs all of them.
resources/views/dashboard.stx:828-842, with the reasoning written out because it looks like a mistake otherwise:
A full document load, deliberately -- this is NOT a swap in disguise. Every goal row carries conversions, converters, rate and total value that come from the LEFT JOIN aggregate in the server block, not from the POST/DELETE response, so there is no client-side shape to re-derive a row from and nothing cheaper to fetch.
It could not be a swap even if there were: the router early-returns on a navigation to the URL it is already on (router client.ts:425), and the
forceargument that bypasses that is not reachable through the two-parameter navigate() the runtime exposes.{ reload: true }skips the router outright by design […]
function refreshFromServer(): void {
navigate(useRoute().fullPath, { replace: true, reload: true })
}Called after every goal mutation — dashboard.stx:863 (addGoal), :887 (deleteGoal).
The same all-or-nothing shape drives every filter interaction on the page: 16 plain <a href="?…"> links (dashboard.stx:1191, 1208, 1222, 1227, 1371, 1388, 1445, 1459, 1475, 1489, 1504, 1523, 1642, 1657, 1671, 1685, 1699). Unticking one filter chip costs a full document load. No optimistic update is possible anywhere in the app.
Evidence in stx
packages/router/src/client.ts:1362-1373 — the whole public surface:
var router={
navigate:navigate,
navigateTo:navigate,
prefetch:function(url){ … },
clearCache:function(){ … },
cache:cache,
swap:swap,
updateNav:updateNav
};No refresh, no reload, no partial. grep -rn 'refresh' packages/router/src/client.ts → no match.
The same-URL early return, packages/router/src/client.ts:510:
if(pushState!==false&&t.href===location.href&&!t.hash&&!force)return Promise.resolve(false);force is the third parameter of function navigate(url,pushState,force) (client.ts:481), and the runtime never supplies it — packages/stx/src/signals.ts:1057-1067:
function navigate(url, options) {
var opts = (options && typeof options === 'object') ? options : { reload: !!options };
var replace = !!opts.replace;
var reload = !!(opts.reload || opts.forceReload);
if (!reload && window.stxRouter && typeof window.stxRouter.navigate === 'function') {
window.stxRouter.navigate(url, { replace: replace }); // ← force never passed
return;
}
…
}useQuery().invalidate() (signals.ts:1292-1296) is not a substitute — it re-runs one client-side query, not the route's server block.
Prior art
- Inertia:
router.reload({ only: ['goals'] })— re-runs the server handler and merges just the named props. - Remix:
useRevalidator().revalidate()— re-runs the current route's loaders, keeping component state. - Next.js App Router:
router.refresh(). - Nuxt:
refreshNuxtData()/useAsyncData().refresh(). - Turbo Frames: refresh a named region rather than the document.
Ask
- Add
router.refresh()(and expose it as a runtime global) that re-requests the current URL as a fragment and swaps it, preserving client signal scopes — i.e. the existing fragment path, with the same-URL early return bypassed internally. - Add a partial form:
router.refresh({ only: ['goals'] }), where the server block can declare named sections and the swap replaces only the matching[data-stx-section]containers. - Failing (2), at minimum make
forcereachable: accept{ force: true }in the runtimenavigateoptions object (signals.ts:1057) and forward it towindow.stxRouter.navigateas the third argument. - Document which of
navigate,refreshand{ reload: true }an app should reach for; today{ reload: true }is the only one that works and it is the most expensive.
Related: #1807 (navigate's second argument), #1772 (hard reload on build-version skew).