Data Fetching & State
Strategy in one line: fetch fresh on the server for every page view, hold client state only where the user is actively typing.
The read path
Both data pages opt out of every cache layer Next.js has:
export const dynamic = "force-dynamic"; // no static prerendering
// ...
const response = await fetch(ENDPOINT, { cache: "no-store" }); // no fetch cache| Mechanism | Effect |
|---|---|
force-dynamic | the route is rendered per-request, never at build time |
cache: "no-store" | the underlying fetch bypasses Next's data cache |
Why: this is an operations dashboard — showing a stale tool-call count defeats the product. The cost (one backend round trip per page view) is trivial locally and acceptable until real traffic exists; adding revalidate windows is the documented escape hatch if that changes.
Error handling on reads
Both pages use the same defensive shape — errors become data, not exceptions:
async function getDashboardData(): Promise<AgentDashboardResponse> {
let response: Response;
try {
response = await fetch(DASHBOARD_ENDPOINT, { cache: "no-store" });
} catch {
throw new Error(`Unable to reach ${DASHBOARD_ENDPOINT}`); // network failure
}
if (!response.ok) {
throw new Error(`Dashboard request failed with status ${response.status}`);
}
return response.json();
}
// in the page component:
try { dashboard = await getDashboardData(); }
catch (error) { errorMessage = error instanceof Error ? error.message : "..."; }The page then branches to an inline error panel with a retry link. Route-level error.tsx (dashboard) and loading.tsx files back this up — but the deliberate catch means the error boundary is a last resort, not the primary UX.
Endpoint configuration
The fetch targets are env-configurable with localhost defaults (Configuration):
| Page | Variable | Default |
|---|---|---|
| Dashboard | AGENTOPS_DASHBOARD_API_URL | http://localhost:8001/agents/1/dashboard |
| Agents | AGENTOPS_AGENTS_API_URL | http://localhost:8001/agents/ |
| Proxy (writes) | AGENTOPS_API_BASE_URL | http://localhost:8001 |
The dashboard/agents variables embed complete URLs (including the agent ID) rather than composing from the base URL — convenient for pointing pages at arbitrary targets, awkward for multi-agent UX. Consolidating on AGENTOPS_API_BASE_URL + route params is the expected refactor (Roadmap).
The write path
Mutations flow browser → /api/agents route handler → FastAPI, documented in API Proxy. After a successful create there is no cache invalidation to do — the agents list is uncached, so the next visit to /agents simply refetches.
State management
The complete client-state inventory of the application:
| Where | State | Why client-side |
|---|---|---|
CreateAgentForm | form, isSubmitting, errorMessage, createdAgent | the user is actively editing; round-tripping keystrokes makes no sense |
That's it. There is no Redux/Zustand/Context, no SWR/React Query, and nothing to hydrate — deliberate, per Design Decisions. Server components re-fetch on navigation; the form owns its own island.
When to add a data library: the moment the UI needs client-side polling/streaming (live dashboards), optimistic updates, or shared cross-page caches. Until then, useState + server components is the whole story.
Typing discipline
Each page declares its own response type (AgentDashboardResponse, AgentResponse) mirroring the backend Pydantic schemas field-for-field. There is no codegen — when a backend schema changes, these types must be updated by hand (checked by npm run typecheck). OpenAPI-based type generation is a natural future improvement.
Related pages
- Pages — the fetching in context
- API Proxy — the write path
- Backend Schemas — the contracts being mirrored