원클릭으로
frontend-large-feature-architecture
You're about to build a **non-trivial frontend feature** —
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
You're about to build a **non-trivial frontend feature** —
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Review code changes and report issues by severity with actionable fixes.
Sharpen a fuzzy intention into one measurable objective string that drives the rest of the work.
Convert a Prompt Flow PRS pipeline submission to run a Microsoft Agent Framework workflow.
Build a Model Context Protocol (MCP) server that lets an LLM call into external tools and resources.
Summarize PDF documents into concise bullet-point digests.
Bump a dependency version across a pnpm workspace and update lockfile.
| name | Frontend Large-Feature Architecture |
| description | You're about to build a **non-trivial frontend feature** — |
| category | code-assistants |
| tags | ["ai","api","backend","cli","database"] |
| source | {"url":"https://github.com/langfuse/langfuse/tree/8624bbf82b06381c461c84e75bdb5fc6626dbb1a/.agents/skills/frontend-large-feature-architecture","fetched_at":"2026-06-12","commit":"8624bbf82b06381c461c84e75bdb5fc6626dbb1a","license":"MIT","original_path":".agents/skills/frontend-large-feature-architecture/SKILL.md"} |
| license | MIT |
| author | Langfuse (downstream pack: badhope) |
| version | 0.1.0 |
| needs_review | false |
| slug | frontend-large-feature-architecture |
| created | 2026-06-12 |
| updated | 2026-06-19 |
| inputs | [{"name":"feature","type":"string","required":true,"description":"One-sentence feature description"},{"name":"framework","type":"string","required":false,"description":"UI framework (default react)"},{"name":"state_libraries","type":"array","required":false,"description":"State libraries already in use (default react-query + zustand)"},{"name":"data_scale","type":"string","required":false,"description":"Data scale - small/medium/large/huge"}] |
| output | {"format":"markdown","description":"Generated content based on the user request"} |
You're about to build a non-trivial frontend feature — something that has more than two screens, more than one fetch, or a piece of state that has to survive navigation. The "just write it and see" approach works for CRUD; for everything else, the wrong choice of state location or component split wastes a sprint.
The skill forces you to decide, before coding, four things that compound once the feature exists:
If you skip these and start with the page component, you'll build the feature three times: once to ship, once to add caching, once to fix perf.
Don't use this skill for trivial CRUD (just use a scaffolder). And don't use it for the visual design itself (use a frontend-design skill or designer).
| Field | Required | Notes |
|---|---|---|
feature | yes | One-sentence feature description. |
framework | no | Defaults to react. Most patterns are framework-agnostic. |
state_libraries | no | What the app already uses. Defaults: react-query + zustand. |
data_scale | no | small / medium / large / huge. Drives virtualization. |
Plain-text architecture plan. Typical return:
State map:
- selectedRowId → URL (?row=...)
- sort + filter state → URL (?sort=...&filter=...)
- "has user pinned" → server (Zustand re-fetched on
change, optimistic)
- dropdown open/closed → local useState
- search query (debounced) → URL + local echo
Component split:
- <TableController> : owns the page-level state,
tRPC call, route params
- <TableToolbar> : sort, filter, search, density
- <TableBody> : virtualization + rows
- <PinRowButton> : presentational, takes
rowId + isPinned as props
- <RowDetailDrawer> : presentational, fetches detail
on demand via tRPC by id
Fetch strategy:
- Initial list: server component with tRPC, streamed
RSC. No client fetch.
- Sort/filter changes: tRPC queries, debounced 250ms
before firing.
- Row pin: client mutation, optimistic update on
list, refetch on success/failure.
- Row detail: client fetch (lazy), cached in
react-query.
Virtualization: tanstack-virtual (window), keep ~50 rows
in DOM, overscan 5.
Edge cases:
- empty list state
- row-pin with row count > 10K (server-side
cursor pagination, not offset)
- filter + sort combination that returns 0 rows
- sort/filter change while a row-detail drawer is
open (close drawer? clear selected id?)
- concurrent pin + unpin (optimistic queueing)
You are planning a non-trivial frontend feature. Decide four
things in order; do not start coding before the plan is
written.
1. Map state to location.
For each piece of state, pick one of:
- URL (?param=value) — shareable, survives refresh,
back button restores it. Use for: filters, sort,
selected id, pagination cursor, view mode.
- Server state (react-query / tRPC) — the source
of truth, owned by the database. Use for: data
fetched from the API.
- Local component state (useState) — UI ephemera.
Use for: dropdown open/closed, hover, debounced
search text, form fields.
- URL state derived from local state — for example
a search input that mirrors a debounced value
in the URL. (The local state drives the input
responsiveness; the URL is the truth for share /
refresh.)
The most common mistake is putting UI ephemera in the
URL, or putting shareable filter state in local state.
Filters go in the URL. "Is this dropdown open" goes
in local state.
2. Pick the component split.
Default to controller + presentational:
- Controller component: owns the page-level state,
fetches the data, derives props, dispatches
mutations.
- Presentational component: receives typed props,
renders. No data fetching, no global state access.
Easier to Storybook, easier to reuse.
One controller per feature. Multiple presentationals.
The controller is the only place that knows the
tRPC / react-query / state hooks for this feature.
If the controller is getting too large (>300 lines),
split by *sub-feature*, not by *sub-component*:
- <PageHeader>
- <PageBody>
- <PageFooter>
Not:
- <PageHeaderController>
- <PageHeaderPresentational>
3. Decide list vs detail fetch.
Initial page load:
- Server component + tRPC + streamed RSC if the
page is read-mostly. No client fetch.
- Client fetch + react-query if the page is
interactive from frame 1 (filters, edits, real
time).
List → detail navigation:
- Lazy: fetch detail only when the user opens
the detail. Cached in react-query. Best for
long lists where most users don't open detail.
- Eager: fetch detail alongside the list. Best
for short lists where most users open detail.
For "open detail in a drawer" patterns, lazy is
almost always right.
4. Decide virtualization.
None — < 100 rows. No virtualization. The
browser handles it.
Window — 100 to 10K rows. tanstack-virtual or
react-window.
Fixed — 10K+ rows. Server-side pagination +
cursor. The client only ever sees
a page of the data.
Custom — irregular row heights, sticky headers
inside rows, etc. Build your own on
top of window.
For a "table" UI in a B2B app, default to window
with server-side pagination behind it. The client
is never asked to render 10K rows in DOM.
5. List edge cases BEFORE coding.
Common ones the planning round catches:
- empty list (zero state, no error, no spinner)
- filter returns zero rows
- filter + sort + cursor pagination (cursor must
respect sort)
- selection persisted in URL but item no longer
exists (404 in drawer, close drawer on mount)
- user mutation while a fetch is in flight
(disable the trigger, queue the action)
- long-running mutation (>10s) — show progress
or a separate "in flight" indicator, not a
blocking spinner
For each edge case, the plan should say:
a) what the user sees
b) which component owns the handling
c) which state flag drives the rendering
<Button>. No
controller needed.<PageHeader>, <PageBody>), not by sub-type
(<PageHeaderController>, <PageHeaderView>).
Type-split components leak state coupling and
make Storybook harder.Input:
feature: |
User can pin a row in the table and the pin persists
across sessions.
framework: react
state_libraries: [react-query, zustand]
data_scale: large
Output:
State map:
- selectedRowId → URL (?row=...)
- sort + filter → URL (?sort=name&filter=active)
- pin state → server (mutation refetches list)
- drawer open/closed → local useState
- search debounce → local echo + URL on debounce
Components:
- <TablePage> controller, owns URL state +
fetches, refetches on mutation
- <TableToolbar> sort/filter/search inputs
- <TableBody> window-virtualized rows
- <PinRowButton> presentational, calls
onPin(rowId) prop
- <RowDetailDrawer> presentational, lazy fetch
via tRPC by id
Fetch strategy:
- list: server component + tRPC streamed
- pin: client mutation, optimistic, refetch on settle
- detail: client fetch on drawer open, react-query
cache by rowId
Virtualization: tanstack-virtual window, 50 in DOM,
overscan 5. Server-side cursor pagination
at 1K rows.
Edge cases:
- empty list → empty state, no error
- filter returns 0 → "no rows match" inline
- selected row id in URL but row deleted → close
drawer on mount if not found
- pin + sort change → sort is independent of pin;
pin order is by pinned_at desc server-side
- concurrent pin/unpin → debounce 250ms; refetch
list on settle
These are the bugs that bite every new user. Check them before shipping:
State in wrong location: Filters in local state instead of URL.
Controller doing too much: Page component with data fetching, state, and UI.
Virtualizing wrong axis: Virtualizing rows when columns are the problem.
Skipping edge cases: Empty states, error states, loading states not planned.
All-in-one mega component: Feature that becomes a 1000-line unmaintainable mess.