Write frontend code
Skill kennguyen887/agent-foundation/skills/write-frontend-code
Claude Code skills marketplace — backend & frontend engineering conventions + step-by-step third-party integration recipes: Stripe, Rapyd, CyberSource, UOB & wallet payments, Singpass/Keycloak OIDC & 3-D Secure, Twilio SMS, Docker & CI/CD. NestJS/TypeScript + React, language-flexible.
npx -y skills add kennguyen887/agent-foundation --skill write-frontend-codeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing FE code inside a feature — data fetching & mutations (React Query), API + DTOs, state (Zustand + Context), custom hooks, forms (RHF + Yup), heavily-used packages, performance, styling (Tailwind + tokens + CSS Modules), config, i18n, SSR/hydration. React/Next.js reference, framework-flexible.
SKILL.md
14.3 KB, as published. Nobody here has run it
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:
// 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: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:// 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 (handleHttpErrorre-throws a consistentErrorwith.response); serialize query params once (qs.stringify(..., { arrayFormat: 'brackets' })). - One service class per entity extends the base; components/queries call the service, never axios:
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():const create: StateCreator<ListingListStore> = (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
queryFnwrapped inunstable_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:
export const useSaveListing = () => { const { handleSubmit } = useFormContext<ListingForm>(); 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). Alwaysuse*-named; memoize derived values.
5. Forms & validation — React Hook Form + Yup
- RHF + Yup via
yupResolver; schema insrc/schemas/<feature>.schema.ts. Dynamic/conditional schemas withyup.when()/yup.lazy().mode: 'onChange'for live feedback. Wrap a form in<FormProvider>, read it in nested fields viauseFormContext(). - Perf on big forms: subscribe to single fields with
useWatch({ name })instead ofwatch()(which re-renders on every keystroke). Wrap inputs in sharedFieldcomponents that showformState.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-uitokens extended intailwind.config.js) — never hard-code hex; use token classes. - Conditional classes via
classnames(aliascn), incl. arbitrary variants:<div className={cn('listing-row', { 'is-error': hasError }, isActive && '[&_svg]:text-primary')} /> - CSS Modules (
*.module.scss) for component-scoped styling beyond utilities; share SCSS vars viastyles/variables.scss.styles/globals.cssholds resets + third-party UI overrides + global overlays. - Integrating a component library? Disable Tailwind preflight to avoid reset clashes. Dark mode
via the
classstrategy. ▸ 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);useCallbackfor handlers passed to memoized children/list rows;useMemofor 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/keepPreviousDatato avoid flicker on param change;staleTime: 0only 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:const Editor = dynamic(() => import('src/components/Editor'), { ssr: false, loading: () => <Spin /> }); - 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 normalizedErrorwith.responsefor 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 sharedhandleError(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:
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: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/rolefrom session/config) → clean up on unmount (logout). Never hard-code ids.
Verification
- Server data: a
queries/<feature>hook (key factory +useQuery); writes via.mutations.tsthatinvalidateQueriesthe right key; no rawfetch/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).