| name | easi-frontend-data |
| description | MUST load when writing or reviewing frontend code that talks to the backend in EASI — HATEOAS link gating, TanStack Query keys, mutation hooks, cache invalidation, multi-surface call-site audits. Load when building action buttons that depend on backend permissions, writing data-fetching hooks, setting up cache invalidation, or adding new mutation hooks. |
| compatibility | opencode |
EASI Frontend Data Layer
Overview
The EASI frontend is HATEOAS-driven: the backend controls what actions are available through _links in API responses. Client code never hardcodes business rules about permissions or role-based action availability. Data fetching uses TanStack Query with a centralized cache invalidation strategy.
For UI components, Mantine usage, styling, and visual layout, load easi-frontend-styling instead. This skill is exclusively about how frontend code reaches the backend.
HATEOAS-Driven UI
Link Type Definition
interface HATEOASLinks {
self?: { href: string; method: string };
edit?: { href: string; method: string };
delete?: { href: string; method: string };
[key: string]: { href: string; method: string } | undefined;
}
All resource types must include _links as a required field:
interface Capability {
id: CapabilityId;
name: string;
_links: HATEOASLinks;
}
HATEOAS Utilities
Use helpers from src/utils/hateoas.ts:
import { hasLink, canEdit, canDelete } from '../utils/hateoas';
if (canEdit(resource)) { }
if (canDelete(resource)) { }
if (hasLink(resource, 'x-children')) { }
Standard Link Relations
| Relation | Purpose |
|---|
self | Current resource URL |
edit | Update resource |
delete | Delete resource |
collection | Parent collection |
x-children | Child resources |
x-remove | Remove from relationship |
x-create-link | Create association |
Conditional Rendering — Gate on Link Presence
{resource._links?.edit && (
<Button onClick={handleEdit}>Edit</Button>
)}
{canDelete(resource) && (
<Button onClick={handleDelete}>Delete</Button>
)}
{userRole === 'admin' && <Button>Edit</Button>}
{!resource.isPrivate && <Button>Edit</Button>}
Cache Invalidation & Mutations (TanStack Query)
Key Files
| File | Purpose |
|---|
src/lib/queryClient.ts | QueryClient config (5min stale time, 30min gc) |
src/lib/queryKeys.ts | Hierarchical query key definitions |
src/lib/mutationEffects.ts | Cache invalidation rules per mutation type |
src/lib/invalidateFor.ts | Helper to invalidate multiple keys atomically |
Query Key Hierarchy
Query keys are hierarchical: all → lists() → detail(id).
When adding a new domain:
- Add its key factory to
src/lib/queryKeys.ts
- Follow the
all → lists() → detail(id) shape
const capabilityKeys = {
all: ['capabilities'] as const,
lists: () => [...capabilityKeys.all, 'list'] as const,
detail: (id: string) => [...capabilityKeys.all, 'detail', id] as const,
};
Mutation Hook Pattern
Standard mutation hooks follow: call API → invalidate cache → show toast.
const mutation = useMutation({
mutationFn: (data) => api.updateCapability(id, data),
onSuccess: () => {
invalidateFor(queryClient, mutationEffects.capabilities.update());
toast.success('Capability updated');
},
onError: (err) => {
toast.error('Failed to update capability');
},
});
Mutation Effects
Each mutation type has defined cache invalidation rules in mutationEffects.ts. When adding a new mutation:
- Define the invalidation rules in
mutationEffects.ts
- Call
invalidateFor(queryClient, mutationEffects.x.y()) in onSuccess
Conditional Queries
Use enabled to wait for required data:
const { data: capability } = useQuery({
queryKey: capabilityKeys.detail(id),
queryFn: () => api.getCapability(id),
enabled: !!id,
});
Static Metadata
Use staleTime: Infinity for data that never changes at runtime:
const { data: metamodel } = useQuery({
queryKey: metamodelKeys.configuration(),
queryFn: api.getMetaModelConfiguration,
staleTime: Infinity,
});
Optimistic Updates
Avoid optimistic updates for domain state. Always wait for server confirmation before updating the UI.
onSuccess: () => {
invalidateFor(queryClient, mutationEffects.capabilities.delete());
}
onMutate: (id) => {
queryClient.setQueryData(capabilityKeys.lists(), (old) =>
old?.filter(c => c.id !== id)
);
}
Multi-Surface Feature Entry Points
Features in EASI can be triggered from multiple surfaces — canvas context menu, navigation tree context menu, toolbar buttons, or keyboard shortcuts. When you add a dialog gate (an interstitial dialog that intercepts an action to collect user input or confirmation), you must update every call site of the underlying action hook, not just the surface you are actively coding.
Audit call sites before implementing a dialog gate
grep -r "yourHookFunction(" frontend/src/ --include="*.tsx" --include="*.ts"
Checklist when adding a dialog gate
State store discipline
- Subscribe to atomic store fields, derive with
useMemo. Avoid selectors that return a fresh object on every render — they invalidate referential equality and cause cascade re-renders.
Reference Implementation
See src/features/components/hooks/useComponents.ts for the canonical example of all these patterns together.
Quick Reference
| Aspect | Pattern |
|---|
| Action availability | Check _links presence — never hardcode |
| Role-based UI | Never check userRole to show/hide actions |
| Query keys | Hierarchical: all → lists() → detail(id) |
| Cache invalidation | Centralized in mutationEffects |
| Mutation structure | mutationFn → invalidateFor → toast |
| Static data | staleTime: Infinity |
| Optimistic updates | Avoid for domain state |
| Conditional queries | enabled: !!dependency |
| Dialog gates | Grep for all call sites and update every surface before marking complete |
Guidelines
- Gate all action visibility on
_links presence — never on role, user ID, or flag
- Use
canEdit, canDelete, hasLink from src/utils/hateoas.ts
- All resource interfaces must include
_links: HATEOASLinks as a required field
- Register all query keys in
src/lib/queryKeys.ts with the hierarchical pattern
- Register all mutation invalidation rules in
src/lib/mutationEffects.ts
- Mutations always call
invalidateFor in onSuccess
- Never use optimistic updates for domain state mutations
- Use
staleTime: Infinity for static metadata queries
- Audit all call sites before adding a dialog gate — grep first, update every entry point, not just the one you are coding
- Subscribe to atomic store fields, derive with
useMemo