Data Dashboard
A dashboard that fetches data from an API, handles loading/error states, and refreshes after mutations. Demonstrates the query cache, HTTP client, and reactive data fetching.
HTTP Client Setup
Create src/lib/http.ts:
ts
import { createHttpClient, createQueryClient } from '@akashjs/http';
export const http = createHttpClient({
baseUrl: '/api',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
export const queryClient = createQueryClient({
defaultStaleTime: 30_000, // 30 seconds
});Page Component
Create src/routes/page.akash:
html
<script lang="ts">
import { signal, computed } from '@akashjs/runtime';
import { useCachedQuery, useMutation } from '@akashjs/http';
import { http, queryClient } from '@/lib/http';
// Reactive filters
const search = signal('');
const page = signal(1);
const pageSize = 10;
// Fetch users — refetches when search or page changes
const users = useCachedQuery(
queryClient,
() => ['users', search(), page()],
() => http.get(`/users?q=${search()}&page=${page()}&limit=${pageSize}`),
{ staleTime: 60_000 },
);
// Delete mutation — invalidates the users query on success
const deleteUser = useMutation(
queryClient,
(id: number) => http.delete(`/users/${id}`),
{
invalidates: ['users'],
onSuccess: () => console.log('User deleted'),
},
);
// Computed stats
const total = computed(() => users()?.total ?? 0);
const totalPages = computed(() => Math.ceil(total() / pageSize));
function handleSearch(e: Event) {
search.set((e.target as HTMLInputElement).value);
page.set(1); // reset to first page on search
}
</script>
<template>
<div class="dashboard">
<h1>Users</h1>
<!-- Search -->
<input
type="search"
placeholder="Search users..."
value={search()}
onInput={handleSearch}
/>
<!-- Loading state -->
<Show when={users.loading() && !users()}>
{() => <div class="loading">Loading users...</div>}
</Show>
<!-- Error state -->
<Show when={users.error()}>
{() => (
<div class="error">
<p>Failed to load users: {users.error()?.message}</p>
<button onClick={() => users.refetch()}>Retry</button>
</div>
)}
</Show>
<!-- Data table -->
<Show when={users()}>
{() => (
<>
<!-- Inline loading indicator for background refetch -->
<Show when={users.loading()}>
{() => <div class="refetching">Refreshing...</div>}
</Show>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<For each={users()?.data ?? []} key={(u) => u.id}>
{(user) => (
<tr>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
<td>
<button
class="danger"
disabled={deleteUser.loading()}
onClick={() => deleteUser.execute(user.id)}
>
Delete
</button>
</td>
</tr>
)}
</For>
</tbody>
</table>
<!-- Pagination -->
<div class="pagination">
<button disabled={page() <= 1} onClick={() => page.update((p) => p - 1)}>
Previous
</button>
<span>Page {page()} of {totalPages()}</span>
<button
disabled={page() >= totalPages()}
onClick={() => page.update((p) => p + 1)}
>
Next
</button>
</div>
</>
)}
</Show>
</div>
</template>
<style scoped>
.dashboard {
max-width: 800px;
margin: 2rem auto;
font-family: system-ui, sans-serif;
}
input[type="search"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
font-weight: 600;
color: #555;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem 0;
}
.pagination button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
cursor: pointer;
}
.pagination button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.loading, .refetching {
padding: 1rem;
text-align: center;
color: #666;
}
.refetching {
font-size: 0.875rem;
color: #6750a4;
}
.error {
padding: 1rem;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 6px;
color: #dc2626;
}
.error button {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background: #dc2626;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
button.danger {
padding: 0.25rem 0.75rem;
background: none;
color: #dc2626;
border: 1px solid #dc2626;
border-radius: 4px;
cursor: pointer;
}
button.danger:hover {
background: #dc2626;
color: white;
}
</style>What This Demonstrates
createHttpClient— typed HTTP client with base URL and credentialscreateQueryClient— centralized cache with stale timeuseCachedQuery— reactive data fetching with cache key tracking- Reactive key:
['users', search(), page()]— refetches when search or page changes staleTime— serves cached data for 60s before refetchingloading()— true during fetch (both initial and background)error()— error state with retryrefetch()— manual refresh
- Reactive key:
useMutation— delete with automatic cache invalidationinvalidates: ['users']— prefix match refetches all user queriesloading()— disables button during mutation
computed()— derived pagination statesignal()— local filter/page state<For>— table rows with keyed reconciliation<Show>— loading, error, and empty states- Fragments
<>...</>— multiple root elements in Show children
