Install
$ agentstack add skill-kennguyen887-agent-foundation-write-frontend-code ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Write frontend code
Examples: React + Next.js (Pages Router), neutral listing domain. principle → ▸ Example → ▸ Other stacks. Where files go: structure-a-frontend-app. General style (early return, pipelines, naming, SOLID, deep copy, casts): code-conventions.
> The data/state stack ships through the shared UI lib. React Query, Zustand, debounce, the HTTP > client, theme tokens etc. are re-exported from @org/web-ui (placeholder for your in-house > design-system package) — so they aren't direct deps of the app. Import from @org/web-ui[/utils| > /store|/queries], not the raw libraries.
1. Data fetching & mutations — React Query
Server data goes through a query layer per feature, never ad-hoc fetch in components. A feature's data folder has four files: .keys.ts (key factory), .queries.ts (reads), .mutations.ts (writes), .types.ts.
- Key factory — namespaced nested arrays, so invalidation is precise:
``ts // listing.keys.ts const listingKeys = { all: ['listing'] as const }; export const listingQuery = { detail: (id?: string) => (id ? [...listingKeys.all, 'detail', id] : [...listingKeys.all, 'detail']), list: (params?: ListListingsRequest) => [...listingKeys.all, 'list', params] as const, }; ``
- Query hook —
enabledguard + per-querystaleTimeoverride +placeholderDatato avoid flicker:
``ts export const useListingDetail = ({ id, options = {} }) => useQuery(listingQuery.detail(id), () => listingService.getDetail(id!), { enabled: !!id, staleTime: 30_000, placeholderData: keepPreviousData, ...options, }); ``
- Mutation hook — invalidate the affected keys in
onSuccess:
``ts // listing.mutations.ts export const usePublishListing = () => { const qc = useQueryClient(); return useMutation(({ id }) => listingService.publish(id), { onSuccess: () => qc.invalidateQueries({ queryKey: listingQuery.detail() }), }); }; ``
- Global client config (once):
staleTime 10m,gcTime 30m,refetchOnWindowFocus false,
retry false, refetchInterval false. Override per-query (staleTime: 0) only for live data. ▸ Other stacks: SWR / TanStack Query; key-factory + hook-per-query + invalidate-on-mutate carries over.
2. API layer — one base client + service classes + DTOs
- One HTTP base client (
BaseHttp, an Axios instance) owns interceptors: attach bearer token;
queue requests during a 401 refresh (concurrent 401s → one refresh, then replay); normalize errors (handleHttpError re-throws a consistent Error with .response); serialize query params once (qs.stringify(..., { arrayFormat: 'brackets' })).
- One service class per entity extends the base; components/queries call the service, never axios:
``ts export class ListingService extends BaseHttp { getList(p: ListListingsRequest) { return this.get('listings', { params: p }).then((r) => r.data); } publish(id: string) { return this.post(listings/${id}/publish).then((r) => r.data); } } ``
- DTOs hand-authored, split
models/request/vsmodels/response/. Build payloads fluently:
const body = Builder(CreateListingBody).title(form.title).price(form.price).build(); ▸ Other stacks: a typed client / generated SDK — one wrapper owns auth + error handling.
3. State — server vs client vs cross-cutting (don't mix)
| Kind | Tool | Use for | |---|---|---| | Server data | React Query (§1) | anything fetched; cache/refetch/invalidate | | Local UI state | Zustand store, one per feature | filters, pagination, selection, wizard step, flags | | Cross-cutting | React Context, set once on boot | auth/session, org/profile, alerts, locale |
- Zustand store shape — State + Actions as separate interfaces, a
resetState, immutable merges viaget():
``ts const create: StateCreator = (set, get) => ({ ...initialState, setFilter: (f) => set({ filter: { ...get().filter, ...f } }), addSelected: (l) => set((s) => s.selected.some((x) => x.id === l.id) ? s : { selected: [...s.selected, l] }), resetState: () => set({ ...initialState }), }); export const [useListingListStore] = createStore(create, { storeName: 'listing-list' }); ``
- Select narrowly +
shallowto avoid full-store re-renders:
const { filter, setFilter } = useListingListStore((s) => pick(s, ['filter','setFilter']), shallow);
- Never copy server data into Zustand (goes stale). If a query must seed store state, do it in the
queryFn wrapped in unstable_batchedUpdates(...) (one re-render, not N). ▸ Other stacks: any store lib + context; the server/client/cross-cutting split is the durable idea.
4. Custom hooks — feature .actions.ts pattern + reusable hooks
- Feature logic lives in a hook, not the component. A feature action hook consolidates form submit
- mutation + navigation + error + UI flags, with a synchronous re-entry guard so double-clicks
can't double-submit: ``ts export const useSaveListing = () => { const { handleSubmit } = useFormContext(); const { setSaving } = useListingListStore(); const { mutateAsync } = usePublishListing(); const busy = useRef(false); const onSave = () => { if (busy.current) return; // sync guard handleSubmit(async (form) => { busy.current = true; setSaving(true); try { await mutateAsync({ id: form.id }); router.push(Routes.Listings); } catch (e) { handleError(e); } finally { busy.current = false; setSaving(false); } })(); }; return { onSave }; }; ``
- Reusable hooks (
src/hooks/general,src/helpers/hooks/domain):useModalState()→
{ visible, show, hide, toggle }; useDebounced(value, ms) (value debounce w/ cleanup); usePagedSearchCombobox({ fetchRows, mapRows, pageSize }) (debounced search + load-more; refs for a fetch-lock + a generation counter to drop stale responses); useMyProfile() (reads a context → memoized role booleans). Always use*-named; memoize derived values.
5. Forms & validation — React Hook Form + Yup
- RHF + Yup via
yupResolver; schema insrc/schemas/.schema.ts. Dynamic/conditional
schemas with yup.when() / yup.lazy(). mode: 'onChange' for live feedback. Wrap a form in `, read it in nested fields via useFormContext()`.
- Perf on big forms: subscribe to single fields with
useWatch({ name })instead ofwatch()
(which re-renders on every keystroke). Wrap inputs in shared Field components that show formState.errors.
6. Packages we lean on
Public OSS named directly; in-house bits come via @org/web-ui.
| Package | For | |---|---| | react-hook-form + @hookform/resolvers + yup | forms + schema validation | | @org/web-ui (+ /utils /store /queries) | design system; React-Query/Zustand wrappers; CommonUtil (debounce, pick, uniqBy), DateUtil | | axios | HTTP (only inside BaseHttp) | | classnames | conditional class strings (§7) | | lodash | utilities — import per-function (import pick from 'lodash/pick') for tree-shaking | | decimal.js | money / precise math (never float) | | builder-pattern | fluent request-DTO construction | | qs / query-string | URL query (de)serialization | | uuid · next-auth · socket.io-client | ids · auth/session · realtime |
▸ Other stacks: swap names, keep the roles (form lib + schema, one HTTP wrapper, a decimal lib, a class-name helper).
7. Styling — Tailwind + tokens + CSS Modules
- Tailwind-first. Colors/spacing/radius/breakpoints come from the shared theme (
@org/web-ui
tokens extended in tailwind.config.js) — never hard-code hex; use token classes.
- Conditional classes via
classnames(aliascn), incl. arbitrary variants:
```tsx
```
- CSS Modules (
*.module.scss) for component-scoped styling beyond utilities; share SCSS vars via
styles/variables.scss. styles/globals.css holds resets + third-party UI overrides + global overlays.
- Integrating a component library? Disable Tailwind preflight to avoid reset clashes. Dark mode
via the class strategy. ▸ Other stacks: utility CSS + scoped modules + one token source.
8. Performance
- Memoize the expensive, not everything.
React.memoon heavy components (editors, charts, media,
big lists); useCallback for handlers passed to memoized children/list rows; useMemo for derived lists/objects. Don't memo trivial leaves.
- Zustand
shallow+ narrow selectors (§3) — usually the biggest re-render win. - React Query tuning: lean on the 10-min
staleTime; narrow withselect;placeholderData/
keepPreviousData to avoid flicker on param change; staleTime: 0 only for truly live data.
- Code-split heavy client-only modules with
next/dynamic+{ ssr: false }(rich-text editor,
embedded reports, video, PDF) — keeps them out of the SSR bundle and initial load: ``ts const Editor = dynamic(() => import('src/components/Editor'), { ssr: false, loading: () => }); ``
- Images:
next/image(lazy + blur placeholder) via a shared wrapper. - Debounce input-driven work (search ~300ms, expensive calc ~400ms, network ~1000ms) via the shared util.
- Lists: pagination by default; add virtualization only when a page truly renders hundreds of rows.
9. Config — typed, public vs private
One typed config module (src/Config.ts): public (client-exposed; NEXT_PUBLIC_*) + private (server-only). Read config through the module, never scatter process.env; safe defaults / fail fast. ▸ Other stacks: a settings module + the framework's public-env prefix.
10. i18n
A localization context + locale JSON with dot-notation namespaces (common.title); a useLanguage(namespace) hook returns t(key). Locale from cookie, switchable. ▸ Other stacks: i18next/react-intl.
11. SSR / hydration gotchas (FE-critical)
- Guard browser-only access:
typeof window !== 'undefined'beforewindow/document/
localStorage; use an isomorphic layout effect.
- SSR-safe defaults, then hydrate (avoids hydration mismatch);
ssr: falsefor components that
can't render server-side.
- Hard navigation (
window.location) for unrecoverable/auth/maintenance states; router push for in-app nav.
12. Error handling & user feedback
A layered stack, not try/catch sprinkled everywhere:
- Global HTTP interceptor (in
BaseHttp, §2) maps transport errors centrally: 401 → queued
refresh + retry; 403 → access message; 503/maintenance → redirect; otherwise → a normalized Error with .response for callers.
- A class
ErrorBoundarycatches render-time errors and shows a fallback per route/region. - Alerts/toasts via context, not per-component state: an
AlertContextkeyed by group id
(openAlert('listing-form', { type: 'error', content })) + a toast/notification util for transient messages. A shared handleError(e) turns a caught error into a user-facing dialog (and logs out on auth-invalidation).
- Form errors surface from
formState.errorsvia the sharedFieldcomponents (§5).
▸ Other stacks: one global request-error handler + an error boundary + a toast/alert channel — don't re-handle the same statuses at every call site. (Auth/session flow: secure-a-frontend-app.)
13. Realtime (sockets)
- One socket client (singleton) owns the lifecycle, not per-component connections:
``ts class SocketClient { private socket?: Socket; init(role: string) { this.socket = io(Config.SOCKET_URL, { auth: { token: BaseHttp.accessToken }, query: { role }, transports: ['websocket'] }); this.socket.on('connect', onConnect); this.socket.on('exception', handleError); } emit(a, p, cb?) { this.socket?.emit(a, p, cb); } on(a, cb) { this.socket?.on(a, cb); } off(a, cb) { this.socket?.off(a, cb); } disconnect() { this.socket?.removeAllListeners(); this.socket?.disconnect(); } } ``
- Init after auth (token available); remove listeners in the
useEffectcleanup and disconnect
on logout. Event names live in an enum; wrap a domain event stream in a context. ▸ Other stacks: a single connection manager + per-effect subscribe/unsubscribe; never leak listeners.
14. Providers & app-init
- Flatten the
_appprovider nest with acombineProviders([A, B, C])helper instead of deep JSX:
``ts const Providers = combineProviders([SessionProvider, QueryProvider, AuthProvider, AlertProvider, OrgProvider]); ``
- Init app-wide side-effects once on mount in a
useConfigs-style hook: logging + a push provider
(e.g. OneSignal) — init() → tag the user (orgId/role from session/config) → clean up on unmount (logout). Never hard-code ids.
Verification
- Server data: a
queries/hook (key factory +useQuery); writes via.mutations.tsthat
invalidateQueries the right key; no raw fetch/axios in components.
- Client state in a per-feature Zustand store, selected with
shallow; no server data duplicated there. - Forms use RHF + a
src/schemas/*.schema.ts; big forms useuseWatchon specific fields. - Styling uses theme tokens +
classnames+*.module.scss; no hard-coded hex. - Heavy client-only modules are
next/dynamic({ ssr: false }); browser APIs aretypeof windowguarded. - No scattered
process.env(all via the Config module). - Transport errors handled once in the HTTP interceptor + an ErrorBoundary + a toast/alert channel — not per call site.
- One socket client owns connect/auth/cleanup; listeners removed in effect cleanup.
Related
secure-a-frontend-app— auth/session, route guards, secrets. ·handle-files-frontend— CSV/PDF/upload/download.structure-a-frontend-app— where these files live.write-frontend-tests·code-conventions·write-service-code(backend equivalent).
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: kennguyen887
- Source: kennguyen887/agent-foundation
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.