Skip to content
3 min read · 572 words

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:

tsx
export const dynamic = "force-dynamic";     // no static prerendering
// ...
const response = await fetch(ENDPOINT, { cache: "no-store" });  // no fetch cache
MechanismEffect
force-dynamicthe 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:

tsx
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):

PageVariableDefault
DashboardAGENTOPS_DASHBOARD_API_URLhttp://localhost:8001/agents/1/dashboard
AgentsAGENTOPS_AGENTS_API_URLhttp://localhost:8001/agents/
Proxy (writes)AGENTOPS_API_BASE_URLhttp://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:

WhereStateWhy client-side
CreateAgentFormform, isSubmitting, errorMessage, createdAgentthe 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.