AppView in a box as a Vite plugin thing
hatk.dev
1---
2title: Data Loading
3description: Load data from your XRPC endpoints in SvelteKit pages using callXrpc and getViewer.
4---
5
6# Data Loading
7
8hatk generates a typed `callXrpc()` function that calls your XRPC endpoints. It works in server load functions, universal load functions, and client-side components -- with full type inference for parameters and return values.
9
10## `callXrpc()`
11
12Import `callXrpc` from `$hatk/client`:
13
14```typescript
15import { callXrpc } from "$hatk/client";
16```
17
18The function signature is:
19
20```typescript
21async function callXrpc<K extends keyof XrpcSchema>(
22 nsid: K,
23 arg?: CallArg<K>,
24 customFetch?: typeof globalThis.fetch,
25): Promise<OutputOf<K>>
26```
27
28The first argument is the XRPC method name (e.g. `"dev.hatk.getFeed"`). TypeScript autocompletes available methods and infers the argument and return types from your lexicons.
29
30**How it works in different contexts:**
31
32- **Browser** -- Makes an HTTP request to `/xrpc/{nsid}` on your server
33- **Server (SSR)** -- Uses an internal bridge to call your XRPC handlers directly, skipping HTTP entirely
34- **Server with `customFetch`** -- Uses the provided fetch function instead of the bridge, which lets SvelteKit deduplicate requests between server and client
35
36## Server load functions
37
38The most common pattern is loading data in `+page.server.ts`. This runs only on the server, so `callXrpc` uses the internal bridge for zero-overhead calls:
39
40```typescript
41// app/routes/+page.server.ts
42import { callXrpc } from "$hatk/client";
43import type { PageServerLoad } from "./$types";
44
45export const load: PageServerLoad = async () => {
46 const feed = await callXrpc("dev.hatk.getFeed", {
47 feed: "recent",
48 limit: 30,
49 });
50 return {
51 items: feed.items ?? [],
52 cursor: feed.cursor,
53 };
54};
55```
56
57The returned data is available in your Svelte component via `$props()`:
58
59```svelte
60<script lang="ts">
61 import type { PageData } from './$types'
62 let { data }: { data: PageData } = $props()
63</script>
64
65{#each data.items as item}
66 <p>{item.status}</p>
67{/each}
68```
69
70## Universal load functions
71
72Universal load functions (`+page.ts`) run on both server and client. Pass SvelteKit's `fetch` as the third argument to `callXrpc` so SvelteKit can deduplicate the request -- on the server it calls your endpoint directly, and on the client it reuses the serialized response instead of making a second HTTP request:
73
74```typescript
75// app/routes/+page.ts
76import { callXrpc } from "$hatk/client";
77import type { PageLoad } from "./$types";
78
79export const load: PageLoad = async ({ fetch }) => {
80 const feed = await callXrpc(
81 "dev.hatk.getFeed",
82 { feed: "recent", limit: 30 },
83 fetch, // SvelteKit's fetch for deduplication
84 );
85 return {
86 items: feed.items ?? [],
87 cursor: feed.cursor,
88 };
89};
90```
91
92### When to use `customFetch`
93
94Pass SvelteKit's `fetch` as the third argument whenever you call `callXrpc` in a universal load function (`+page.ts` or `+layout.ts`). This tells `callXrpc` to skip the internal server bridge and use SvelteKit's fetch instead, which handles request deduplication between server rendering and client hydration.
95
96You don't need `customFetch` in `+page.server.ts` files -- those only run on the server, where the bridge is faster.
97
98## Client-side data loading
99
100You can also call `callXrpc` directly in components for client-side fetching, such as infinite scroll:
101
102```svelte
103<script lang="ts">
104 import { callXrpc } from '$hatk/client'
105
106 let items = $state(data.items)
107 let cursor = $state(data.cursor)
108 let loadingMore = $state(false)
109
110 async function loadMore() {
111 if (!cursor || loadingMore) return
112 loadingMore = true
113 try {
114 const res = await callXrpc('dev.hatk.getFeed', {
115 feed: 'recent',
116 limit: 30,
117 cursor,
118 })
119 items = [...items, ...res.items]
120 cursor = res.cursor
121 } finally {
122 loadingMore = false
123 }
124 }
125</script>
126```
127
128When called from the browser, `callXrpc` makes a standard HTTP request to your `/xrpc/{nsid}` endpoint.
129
130## Identifying the current user
131
132### `parseViewer()` in layout loads
133
134Use `parseViewer()` in your root layout to read the session cookie and make the current user available to all pages:
135
136```typescript
137// app/routes/+layout.server.ts
138import { parseViewer } from "$hatk/client";
139import type { LayoutServerLoad } from "./$types";
140
141export const load: LayoutServerLoad = async ({ cookies }) => {
142 const viewer = await parseViewer(cookies);
143 return { viewer };
144};
145```
146
147`parseViewer` decrypts the session cookie and returns `{ did, handle? }` for authenticated users, or `null` for anonymous visitors. The result flows into every page's `data.viewer`.
148
149### `getViewer()` in server functions
150
151Use `getViewer()` to access the current user in remote functions and other server-side code that runs within a request:
152
153```typescript
154import { getViewer } from "$hatk/client";
155
156const viewer = await getViewer();
157if (!viewer) throw new Error("Not authenticated");
158// viewer.did is the user's DID
159```
160
161`getViewer()` reads the viewer that was set by `parseViewer` earlier in the request lifecycle. It returns `{ did }` or `null`.
162
163## Types from `$hatk/client`
164
165The generated client re-exports all view and record types from your lexicons:
166
167```typescript
168import type { StatusView, ProfileView } from "$hatk/client";
169```
170
171These types are derived from your lexicon definitions, so they stay in sync when you change a lexicon and run `hatk generate types`.