Data Fetching (useData)
ubean provides useData() for declarative data fetching with caching, TTL, and invalidation. It is auto-imported from ubean/client (in the UBEAN_CLIENT_PRESET).
ubean does not provide
defineLoader. UseuseData()inside<script setup>for server-side or client-side data fetching.
Basic Usage
useData takes a single options object — key (a stable string) plus fetcher (the async function that loads the data). It is awaited directly in <script setup>:
<script setup lang="ts">
// useData is auto-imported
const { data, error, loading, refresh, invalidate } = await useData({
key: 'posts',
fetcher: () => fetch('/api/posts').then(r => r.json())
});
</script>
<template>
<div v-if="loading">Loading…</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="post in data.posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>Async Fetcher
<script setup lang="ts">
const { data } = await useData({
key: 'user',
fetcher: async () => {
const res = await fetch('/api/user');
return res.json();
}
});
</script>Reading Route Params
Use useRouter() (import from vue-router, or enable autoImports: { vueRouter: true }) to read route params inside the fetcher:
<script setup lang="ts">
const router = useRouter();
const { data: post } = await useData({
key: `post-${router.currentRoute.value.params.id}`,
fetcher: () => fetch(`/api/posts/${router.currentRoute.value.params.id}`).then(r => r.json())
});
</script>Multiple Sources
Combine multiple useData calls:
<script setup lang="ts">
const { data: posts } = await useData({
key: 'posts',
fetcher: () => fetch('/api/posts').then(r => r.json())
});
const { data: user } = await useData({
key: 'user',
fetcher: () => fetch('/api/user').then(r => r.json())
});
</script>Dependent Data
Cache keys are static strings — useData fetches once per key per mount. To refetch when a dependency changes, call refresh() (e.g., from a watcher):
<script setup lang="ts">
import { computed, watch } from 'vue';
const router = useRouter();
const postId = computed(() => router.currentRoute.value.params.id as string);
const { data: post, refresh } = await useData({
key: `post-${postId.value}`,
fetcher: () => fetch(`/api/posts/${postId.value}`).then(r => r.json())
});
// Refetch when the route param changes
watch(postId, () => refresh());
</script>Caching & TTL
<script setup lang="ts">
const { data } = await useData({
key: 'config',
fetcher: () => fetch('/api/config').then(r => r.json()),
ttl: 60_000, // Cache for 60 seconds
tags: ['config'], // Tag for group invalidation (invalidateData('config'))
dedupe: true // Share in-flight requests with the same key (default)
});
</script>Refresh & Invalidation
<script setup lang="ts">
const { data, refresh, invalidate } = await useData({
key: 'posts',
fetcher: fetchPosts
});
async function handleRefresh() {
await refresh();
}
// Invalidate this key (refetches on next access)
function handleInvalidate() {
invalidate();
}
</script>Error Handling
<script setup lang="ts">
const { data, error } = await useData({
key: 'posts',
fetcher: async () => {
const res = await fetch('/api/posts');
if (!res.ok) {
throw new Error(`Failed: ${res.status}`);
}
return res.json();
}
});
</script>
<template>
<div v-if="error" class="error">{{ error.message }}</div>
</template>Client-Side Only Fetching
For data that should only load on the client (after hydration), use Vue’s onMounted:
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const data = ref(null);
onMounted(async () => {
const res = await fetch('/api/analytics');
data.value = await res.json();
});
</script>API Routes
For server-side data, prefer API routes with defineHandler:
// src/routes/api/posts.ts
import { defineHandler } from 'ubean/server';
export const GET = defineHandler(async c => {
const posts = await fetchPostsFromDB();
return c.json({ posts });
});Best Practices
- Stable keys: Use stable string keys to enable caching across components
- Handle errors: Always handle the
errorstate in templates - Use TTLs: Set TTLs for data that changes infrequently
- Invalidate after mutations: Call
invalidate()after writes to refetch - SSR payloads: Data fetched via
useDataduring SSR is serialized into the__UBEAN_DATA__payload and hydrated on the client automatically — no extra config needed
useFetch
useFetch wraps useAsyncData. It does not invent an HTTP client: pass a duck-typed @soybeanjs/fetch instance via setDefaultFetch(createRequest()) (or options.request). Without a client it falls back to fetch + JSON.
<script setup lang="ts">
import { createRequest } from '@soybeanjs/fetch';
setDefaultFetch(createRequest());
const { data, pending, refresh } = await useFetch('posts', '/api/posts');
</script>