app-renderer-systems

$npx mdskill add compozy/agh/app-renderer-systems

Guides creation and modification of domain feature systems under systems/ directory.

  • Adds or extends a domain system with API, queries, hooks, and components.
  • Depends on Read, Grep, Glob tools and companion skills like react and tanstack.
  • Uses directory-layout.md and patterns.md references to enforce conventions.
  • Outputs code changes following barrel exports, service layers, and store patterns.

SKILL.md

.github/skills/app-renderer-systemsView on GitHub ↗
---
name: app-renderer-systems
description: Guides creation and modification of domain feature systems organized under a systems/ directory. Covers directory layout, API service layer patterns, TanStack Query hooks (queries, mutations, optimistic updates), React context and XState store conventions, hook organization, and public API barrel exports. Use when adding a new domain system, extending an existing one, or fixing bugs in a system-layer codebase. Don't use for generic React component work, backend API implementation, or codebases not organized around a systems/ domain pattern.
allowed-tools: Read, Grep, Glob
---

# Feature Systems Guide

A "system" is a self-contained, domain-driven module that owns everything related to one domain: its API calls, query layer, hooks, components, and public API. Systems live under a `systems/<domain>/` directory.

Read `references/directory-layout.md` for the full directory structure and naming conventions.
Read `references/patterns.md` for annotated implementation patterns per layer.

## Quick Reference

### Mandatory Companion Skills

Activate alongside this skill — systems span multiple technical domains:

| Situation             | Activate                                  |
| --------------------- | ----------------------------------------- |
| Any hook or component | `react` + `tanstack`                      |
| Data fetching/caching | `agh-data-boundaries` + `tanstack`        |
| Mutations             | `tanstack`                                |
| XState store          | `xstate`                                  |
| Utility functions     | `es-toolkit`                              |
| Writing/fixing tests  | `test-antipatterns` + `vitest`            |
| Bug fix               | `systematic-debugging` + `no-workarounds` |

### System Directory at a Glance

```
systems/<domain>/
├── index.ts               # Public API barrel — required for every system
├── types.ts               # TypeScript types for this domain
├── adapters/              # API service layer (HTTP calls, error types)
│   └── <domain>-api.ts
├── lib/                   # Pure utilities, schemas, constants, query keys
│   ├── query-keys.ts      # TanStack Query key factory
│   ├── query-options.ts   # Reusable queryOptions / mutationOptions
│   ├── <domain>-schemas.ts
│   └── constants.ts
├── hooks/                 # React hooks (queries, mutations, view-models)
│   ├── __tests__/
│   ├── use-<action>.ts    # Query hooks
│   ├── use-create-<entity>.ts  # Mutation hooks
│   ├── use-update-<entity>.ts
│   ├── use-delete-<entity>.ts
│   └── use-<domain>-view-model.ts
├── contexts/              # React contexts + providers
│   └── <domain>-context.tsx
├── stores/                # XState stores (complex async state machines)
│   └── <domain>-store.ts
├── components/            # React UI components
│   ├── stories/
│   └── index.ts
└── guards/                # Route guards / access checks
```

## Step-by-Step: Creating a New System

### Step 1 — Define types.ts

- Export clean component view-model types; preserve API page envelopes in adapters and query caches, then flatten at the hook/view-model read boundary.
- Derive from the project's API contract types when available.
- Document complex aggregated types with JSDoc explaining derivation rules and invariants.

### Step 2 — Build the API service layer

- Create `adapters/<domain>-api.ts`.
- Use the project's HTTP client for API calls.
- Export a single namespace object: `export const <domain>Api = { list, create, update, delete }`.
- Export a typed error class: `export class <Domain>ApiError extends Error { ... }`.
- Accept `signal?: AbortSignal` on every function to support query cancellation.
- Keep all internal helpers (error extraction, response normalization) private to the module.

### Step 3 — Add lib/query-keys.ts

```ts
export const <domain>Keys = {
  all: ["<domain>"] as const,
  lists: () => [...<domain>Keys.all, "list"] as const,
  list: (scopeId: string | null) => [...<domain>Keys.lists(), scopeId] as const,
  details: () => [...<domain>Keys.all, "detail"] as const,
  detail: (id: string) => [...<domain>Keys.details(), id] as const,
};
```

- Use hierarchical key structure for granular invalidation.
- Scope keys with any identifier (userId, orgId, etc.) that isolates the cache correctly.
- Use `as const` on every key tuple.

### Step 4 — Add lib/query-options.ts

```ts
import { queryOptions } from "@tanstack/react-query";
import { <domain>Api } from "../adapters/<domain>-api";
import { <domain>Keys } from "./query-keys";

export function <domain>ListOptions(scopeId: string | null) {
  return queryOptions({
    queryKey: <domain>Keys.list(scopeId),
    queryFn: ({ signal }) => <domain>Api.list(scopeId!, signal),
    staleTime: 60_000,
    enabled: Boolean(scopeId),
  });
}

export function <domain>DetailOptions(id: string) {
  return queryOptions({
    queryKey: <domain>Keys.detail(id),
    queryFn: ({ signal }) => <domain>Api.get(id, signal),
    enabled: Boolean(id),
  });
}
```

- Co-locate `queryKey` and `queryFn` via `queryOptions` for type safety and reuse.
- Export each option factory for hooks, prefetching, route loaders, mutations, and stream writers.
- Keep stable scope/filters in the base key and continuation cursors only in `pageParam`.
- Always pass `signal` from the query context through to the API layer.

### Step 5 — Write hooks

- **Query hooks**: Wrap `useQuery` with the `queryOptions` factories; accept a scope ID + optional `{ enabled? }`.
- **Mutation hooks**: Use `useMutation` with proper `onMutate` / `onError` / `onSettled` callbacks for optimistic updates.
- **View-model hooks**: Compose multiple hooks for a page/shell component; return a flat object.
- Place tests in `hooks/__tests__/` or co-locate as `use-xxx.test.tsx`.

Read `references/patterns.md` for complete mutation and optimistic update patterns.

### Step 6 — (Optional) Add context

Create `contexts/<domain>-context.tsx` when query data or combined state must be shared across a component subtree without prop-drilling.

```ts
// Always nullable context — consumer hook throws if used outside provider
export const <Domain>Context = createContext<<Domain>ContextValue | null>(null);
```

- Export the context, provider component, and re-export consumer hooks from the same file.
- For performance-sensitive trees, split into Core / UI / Operations sub-contexts.

### Step 7 — (Optional) Add an XState store

Create `stores/<domain>-store.ts` for complex async state machines (multi-step flows, polling, event emission).

```ts
export const <domain>Store = createStore({
  context: { ... } as <Domain>Context,
  emits: { ... },
  on: {
    someEvent: (context, event, enqueue) => {
      enqueue.effect(async () => { ... });
      return { ...context, isLoading: true };
    },
  },
});
```

### Step 8 — Wire up index.ts

Organize the barrel with labeled sections and explicit named exports:

```ts
// Types
export type { <Domain>Type } from "./types";

// Hooks
export { use<Domain>List, use<Domain>Detail } from "./hooks";
export { useCreate<Domain>, useUpdate<Domain>, useDelete<Domain> } from "./hooks";

// Components
export { <Domain>Component } from "./components";

// Utilities
export { <domain>HelperFn } from "./lib/<domain>-utils";

// Query Keys & Options
export { <domain>Keys } from "./lib/query-keys";
export { <domain>ListOptions, <domain>DetailOptions } from "./lib/query-options";

// API
export { <domain>Api, <Domain>ApiError } from "./adapters/<domain>-api";
```

## Critical Rules

1. **Use `queryOptions` for co-location.** Reuse option/key factories across loaders, hooks, mutations, and streams; never redeclare server-query identity at a consumer.
2. **Unidirectional dependency flow.** `adapters -> lib -> hooks -> components`. Adapters never import from hooks or components.
3. **Scope query keys.** Include every stable scope/filter in the base key and keep pagination cursors in `pageParam` to prevent cross-scope data and cache fragmentation.
4. **Typed errors in the API layer.** Never throw raw errors from adapters. Use a typed error class so consumers can distinguish error types without inspecting message strings.
5. **AbortSignal propagation.** Pass `signal` from the `queryFn` context through to every API call for proper query cancellation.
6. **Follow the owner's mutation contract.** Reconcile through canonical keys; invalidate on settle only when the read model requires a server reread.
7. **Optimistic updates require rollback.** When using cache-based optimistic updates, snapshot previous data in `onMutate` and restore in `onError`.
8. **Cancel outgoing queries before optimistic updates.** Call `queryClient.cancelQueries` in `onMutate` to prevent refetches from overwriting optimistic state.
9. **Zod schemas in lib/.** Place all Zod schemas in `lib/<domain>-schemas.ts` for runtime validation at API boundaries.

## Error Handling

- **API layer throws typed error**: TanStack Query catches and exposes it via `query.error`.
- **Mutation fails with optimistic update**: `onError` restores the complete cache snapshot; `onSettled` rereads only when the owner contract requires it.
- **Stale cross-scope data**: Add the scope ID to the query key and verify that `enabled` guards check `Boolean(scopeId)`.
- **Query cancellation on unmount**: TanStack Query automatically cancels in-flight queries via the `signal` when a component unmounts — ensure `signal` is propagated to the API layer.

## Detailed References

- `references/directory-layout.md` — Full directory structure, file naming, and barrel conventions
- `references/patterns.md` — Annotated code patterns for the API layer, query options, hooks, mutations, optimistic updates, contexts, and stores

More from compozy/agh

SkillDescription
agent-exploration>-
aghAGH runtime and contribution guide. Use for sessions, agents, native tools, skills, memory, network, tasks, capabilities, bundles, QA, docs, and repo work. Do not use for unrelated projects.
agh-cleanup-failure-paths>-
agh-code-guidelines>-
agh-contract-codegen-coshipContract co-ship for AGH wire changes. Use when editing public DTOs, OpenAPI, JSON-RPC extension shapes, generated clients, or handler request/response semantics. Do not use for internal Go refactors or test-only changes that leave the wire contract unchanged.
agh-data-boundariesData-boundary audit for AGH route loaders, TanStack Query caches, paginated catalogs, filters, ordering, totals, live streams, workspace-scoped reads, and their backend APIs. Use when adding, changing, or debugging those paths. Don't use for presentational-only UI or backend work with no public read-model impact.
agh-designAGH visual-design authority for production UI, static artifacts, prototypes, and reviews. Use when creating or reviewing an AGH surface or changing tokens, typography, spacing, depth, icons, or motion. Do not use for capture-only verification; use agh-ui-screenshot.
agh-qa-bootstrapBootstraps isolated AGH QA labs and emits the canonical manifest for downstream QA. Use when local QA needs daemon, workspace, provider, or playbook setup. Do not use for unit tests, planning-only work, or browser-only checks.
agh-schema-migrationAuthors append-only Goose SQL migrations for AGH SQLite streams and keeps declarative schema sources, atlas.sum, Atlas checks, sqlc output, and migration tests synchronized. Use for SQLite table, column, index, constraint, trigger, or seed-data changes under internal/store or internal/memory. Do not use for in-memory structures, derived Markdown memory, or non-SQLite caches.
agh-test-conventions>-