with one click
frontend
Frontend development rules and patterns for Next.js/React projects
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Frontend development rules and patterns for Next.js/React projects
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | frontend |
| description | Frontend development rules and patterns for Next.js/React projects |
any unless absolutely necessarytype instead of interface for simple type definitionstype import for interfaces exported from modulessrc/types folder, organized by domainPick, Omit, Partial) for reusable patternsDocumentGetting.tsx → document-getting.tsx, useDocument → use-document.ts)See code-style.mdc for full details.
components/components/ui/components/[feature]/See react.mdc for full details.
rem units for spacing, sizing, and typography — never pxSee ui.mdc for full details.
See layout.mdc for full details (form placement, data-fetching states, grid rules).
Every data-fetching component must handle:
Loading — Show skeleton placeholder matching the content layout:
if (!data) return <Skeleton />;
Empty — Show empty state message:
<EmptyState isEmpty={data.length === 0}>
<EmptyStateContent>
<EmptyStateTitle>title</EmptyStateTitle>
<EmptyStateDescription>description</EmptyStateDescription>
</EmptyStateContent>
<EmptyStateConceal>
<Content data={data} />
</EmptyStateConceal>
</EmptyState>
Success — Render the data inside <EmptyStateConceal>
Prefer shadcn Skeleton component for loading.
See form.mdc and form.dialog.mdc for full form and dialog patterns.
initialFormData (optional), isLoading (optional), onSubmit (required), explicit defaultState, ref for form.reset()form.formState.isSubmitting is truesonner:
toast.success("Record created successfully");
toast.error("Error creating a record", { description: 'Error Message' });
[FormName]Dialog (e.g., CaseFormDialog.tsx in components/[feature]/)
open/onOpenChange statechildren: React.ReactNode (trigger button element)[FormName]Integrated inside DialogContent[FormName]Integrated (e.g., CaseFormIntegrated.tsx in components/[feature]/)
onClose: () => void[FormName]Form with submission handler[FormName]Form (e.g., CaseForm.tsx in components/forms/)
initialFormData?, submitHandler, ref?See table.mdc for full TanStack Table rules via AppDataTable.
See tanstack-router.mdc for routing conventions.
See next-js.mdc for Next.js-specific rules.
hooks/See friendly-errors.md for user-friendly error patterns.
const formatPrice = (price: number) =>
new Intl.NumberFormat("en-NG", {
style: "currency",
currency: "NGN",
maximumFractionDigits: 0,
}).format(price);
Returns: "₦1,500"
// Query
const data = useQuery(api.myFunction, { arg });
// Mutation
const mutate = useMutation(api.myMutation);
mutate({ arg });
See form.mdc for full form generation rules.
See form.dialog.mdc for the full form-in-dialog pattern.
Tabs, TabsList, TabsTrigger, TabsContentInstead of repeated find() calls in JSX:
// Bad - repeated find in JSX
<Form initialData={currentPlanId ? plans.find(p => p.id === currentPlanId) : undefined} />
// Good - extract to variable
const currentPlan = plans.find(p => p.id === currentPlanId);
<Form initialData={currentPlan} />
const computed = React.useMemo(() => expensiveOperation(data), [data]);
Return early to reduce nesting:
if (!data) return <Skeleton />;
return ( /* main content */ );
Avoid creating separate state for derived values:
// Bad - duplicate state
const [filtered, setFiltered] = useState(data);
useEffect(() => setFiltered(data.filter(...)), [data]);
// Good - derive in render
const filtered = data.filter(...);
// Bad
<Component a={obj.a} b={obj.b} c={obj.c} d={obj.d} />
// Good
const { a, b, c, d } = obj;
<Component a={a} b={b} c={c} d={d} />
// Or as spread
<Component {...pick(obj, 'a', 'b', 'c', 'd')} />
Forms should use local isSubmitting state passed to isLoading prop:
const [isSubmitting, setIsSubmitting] = useState(false);
<Form
isLoading={isSubmitting}
onSubmit={async (values) => {
setIsSubmitting(true);
try {
await mutation(values);
} finally {
setIsSubmitting(false);
}
}}
/>
// Form button shows "Saving..." when isLoading=true
Inline functions cause child components to rerender on every parent render:
// Bad - creates new function each render
<Button onClick={() => handleClick(id)} />
// Good - use stable handler, pass id as argument
<Button onClick={handleClick} payload={id} />
Prefer map with direct access over multiple/nested ternary operations for JavaScript conditionals.
// Bad - nested ternaries
const value = var === "a" ? 1 : var === "b" ? 2 : var === "c" ? 3 : "default";
// Good - map lookup
const valueMap = {
"a": 1,
"b": 2,
"c": 3
};
const value = valueMap[var] || "default";
// Show/Hide with && (for no else case)
{isOpen && <Content />}
// Ternary for else case
{isOpen ? <OpenState /> : <ClosedState />}
Group related components for cleaner exports:
// Instead of
export { Button } from "./button";
export { ButtonGroup } from "./button-group";
// Use compound component
export const Button = Object.assign(ButtonRoot, { Group: ButtonGroup });
// Usage: <Button.Group>
bun format, pnpm format