AppView in a box as a Vite plugin thing hatk.dev
at main 171 lines 5.5 kB view raw view rendered
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`.