Route Data Loading
WinJS provides a route-level data loading mechanism clientLoader: export an extra clientLoader function from a page file, it runs automatically after the route component module is ready, and its return value is consumed in the component via useLoaderData(). Vue 3 / Vue 2 / Svelte framework chains share the same semantics.
It turns the sequential flow of "mount the component, then fetch data in a lifecycle hook" into a declarative "load component module → run loader → render" flow: a loading boundary renders while loading, and the page component renders only after the data is ready.
Enabling
Enable via config:
// .winrc.ts
export default {
clientLoader: {}
}Usage
Vue 3
Export clientLoader from the plain <script> block of a .vue page (it can coexist with <script setup>, which does not allow export):
<script>
export async function clientLoader({ route, params, query }) {
const res = await fetch(`/api/users/${params.id}`);
return { user: await res.json() };
}
</script>
<script setup>
import { useLoaderData } from 'winjs';
const data = useLoaderData();
</script>
<template>
<h2 v-if="data">{{ data.user.name }}</h2>
</template>useLoaderData() returns a computed ref, which is undefined while pending or on failure.
Vue 2
The page syntax is the same as Vue 3 (export from the plain <script>). Since Vue 2 has no Composition API, wrap useLoaderData() in a computed for reactivity:
<script>
export async function clientLoader({ route, params, query }) {
const res = await fetch(`/api/users/${params.id}`);
return { user: await res.json() };
}
</script>
<script setup>
import { computed } from 'vue';
import { useLoaderData } from 'winjs';
const data = computed(() => useLoaderData());
</script>
<template>
<h2 v-if="data">{{ data.user.name }}</h2>
</template>Svelte
Export from the module-level <script> of the page (exports in the instance <script> are component props):
<script module lang="ts">
export async function clientLoader({ params, query }: any) {
const res = await fetch(`/api/users/${params.id}`);
return { user: await res.json() };
}
</script>
<script lang="ts">
import { useLoaderData } from 'winjs';
const loaderData = useLoaderData();
const data = $derived($loaderData);
</script>
{#if data}
<h2>{data.user.name}</h2>
{/if}JS / TS pages
For non-SFC pages (.ts / .tsx / .js / .jsx), use a named export:
export async function clientLoader({ params, query }) {
const res = await fetch(`/api/users/${params.id}`);
return { user: await res.json() };
}Loader signature
Unified across the three framework chains:
export async function clientLoader(ctx: {
route: RouteLocation; // current route (native route object of each framework)
params: Record<string, string>;
query: Record<string, string>;
}) {
return data; // any serializable value, consumed via useLoaderData()
}Loading boundary
While the loader is running, a global loading component renders. Provide it via the convention file src/loading.(js|ts) (default-export a component); when absent, the boundary renders nothing:
// src/loading.js
import { h } from 'vue';
export default {
render() {
return h('div', 'loading...');
}
};When the loader throws, the boundary renders an error message (Failed to load data: <message>) and the page component does not render; the same applies to route component module load failures (Failed to load route: <message>).
Caching semantics
Loader results are cached by "leaf route path + full location (query included)":
- Repeated navigation to the same location (including going back/forward to it) hits the cache without re-execution
- A query change (e.g.
/users?page=1→/users?page=2) counts as a new location and re-executes - Failed results are cached too; repeated navigation to them keeps showing the error
To force-refresh data, carry a timestamp or similar marker in the query.
Notes
- In
.vuepages,clientLoadermust be declared in a plain<script>block (<script setup>does not allowexport); a<script setup>-only page needs an additional plain<script>block - A
.vuepage with only a plain<script>(no<script setup>) still needsexport defaultfor the page component itself - The loader runs after the route component module is ready (sequential with rendering); parallel prefetching across nested route levels is not supported yet
