| name | frontend |
| description | Frontend development rules and patterns for Next.js/React projects |
Frontend Skill
Tech Stack
- Next.js 15 (App Router)
- React 19
- Shadcn UI components
- TanStack Table + TanStack Query + TanStack Router
- Effect-ts
- React Hook Form + Zod
- Lucide React icons
- Convex (backend)
- date-fns
- sonner (toasts)
- Motion (for animations)
Conventions
TypeScript
- Always type React components with explicit interfaces
- Never use
any unless absolutely necessary
- Use
type instead of interface for simple type definitions
- Ensure convex functions have proper type definitions
- Prefer
type import for interfaces exported from modules
- Use path aliases for shorter imports
- Default exports should be last statement in every module
- Place shared interfaces in
src/types folder, organized by domain
- Use intersection types, generic types, and utility types (
Pick, Omit, Partial) for reusable patterns
- Use discriminated unions for type safety and clarity
Naming Convention
- Use kebab-case for file names (e.g.,
DocumentGetting.tsx → document-getting.tsx, useDocument → use-document.ts)
Code Style
See code-style.mdc for full details.
Import Order
- React and Next.js imports
- External libraries and dependencies
- Convex-specific imports
- Internal components and utilities
- Type imports
- CSS/style imports
Component Structure
- Components:
components/
- ShadcnUI derived components:
components/ui/
- Feature components:
components/[feature]/
Component Patterns
See react.mdc for full details.
UI Guidelines
See ui.mdc for full details.
Layout
See layout.mdc for full details (form placement, data-fetching states, grid rules).
Data Fetching: Three States
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.
Grids for Catalogs
- 5 columns for xl screens
- 4 columns for lg
- 3 columns for tablet
- 1 column for mobile
Form Pattern
See form.mdc and form.dialog.mdc for full form and dialog patterns.
Form Generation Rules
- Use Shadcn Form components
- Use react-hook-form for form state management
- Use Zod for validation
- Each form must be in its own file
- Forms must be pure components
- Props:
initialFormData (optional), isLoading (optional), onSubmit (required), explicit defaultState, ref for form.reset()
- Validation logic and schema must be in the same module
- Form default state must be provided outside the component scope
Submit Button
- Must be disabled + reflect pending state text (e.g., "Creating..." instead of "Create Form") when
form.formState.isSubmitting is true
After Submission
Form Dialog Components
-
[FormName]Dialog (e.g., CaseFormDialog.tsx in components/[feature]/)
- Dialog wrapper that manages its own
open/onOpenChange state
- Props:
children: React.ReactNode (trigger button element)
- Renders
[FormName]Integrated inside DialogContent
-
[FormName]Integrated (e.g., CaseFormIntegrated.tsx in components/[feature]/)
- Encapsulates form logic, submission handling, and data interaction
- Props:
onClose: () => void
- Renders
[FormName]Form with submission handler
-
[FormName]Form (e.g., CaseForm.tsx in components/forms/)
- Pure form component — rendering, input management, validation only
- Props:
initialFormData?, submitHandler, ref?
Tables
See table.mdc for full TanStack Table rules via AppDataTable.
Routing
See tanstack-router.mdc for routing conventions.
Next.js
See next-js.mdc for Next.js-specific rules.
Hooks
- Path:
hooks/
- Use for reusable state/logic
State Management
- TanStack Query: server state, API calls
- React useState: local component state
- Zustand: global client state
Currency Formatting (Naira)
const formatPrice = (price: number) =>
new Intl.NumberFormat("en-NG", {
style: "currency",
currency: "NGN",
maximumFractionDigits: 0,
}).format(price);
Returns: "₦1,500"
TanStack Query Usage
const data = useQuery(api.myFunction, { arg });
const mutate = useMutation(api.myMutation);
mutate({ arg });
React Hook Form + Zod
See form.mdc for full form generation rules.
Alerts and Dialogs
See form.dialog.mdc for the full form-in-dialog pattern.
Tabs
- Use shadcn
Tabs, TabsList, TabsTrigger, TabsContent
Code Optimization
Extract Repeated Lookups
Instead of repeated find() calls in JSX:
<Form initialData={currentPlanId ? plans.find(p => p.id === currentPlanId) : undefined} />
const currentPlan = plans.find(p => p.id === currentPlanId);
<Form initialData={currentPlan} />
Memoize Expensive Computations
const computed = React.useMemo(() => expensiveOperation(data), [data]);
Early Return Pattern
Return early to reduce nesting:
if (!data) return <Skeleton />;
return ( );
Derived State in JSX
Avoid creating separate state for derived values:
const [filtered, setFiltered] = useState(data);
useEffect(() => setFiltered(data.filter(...)), [data]);
const filtered = data.filter(...);
Object Extraction for Repeated Props
<Component a={obj.a} b={obj.b} c={obj.c} d={obj.d} />
const { a, b, c, d } = obj;
<Component a={a} b={b} c={c} d={d} />
<Component {...pick(obj, 'a', 'b', 'c', 'd')} />
Form Component Props Pattern
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);
}
}}
/>
Avoid Inline Arrow Functions in JSX
Inline functions cause child components to rerender on every parent render:
<Button onClick={() => handleClick(id)} />
<Button onClick={handleClick} payload={id} />
Conditional Rendering with Maps
Prefer map with direct access over multiple/nested ternary operations for JavaScript conditionals.
const value = var === "a" ? 1 : var === "b" ? 2 : var === "c" ? 3 : "default";
const valueMap = {
"a": 1,
"b": 2,
"c": 3
};
const value = valueMap[var] || "default";
Conditional Rendering
{isOpen && <Content />}
{isOpen ? <OpenState /> : <ClosedState />}
Compound Component Pattern
Group related components for cleaner exports:
export { Button } from "./button";
export { ButtonGroup } from "./button-group";
export const Button = Object.assign(ButtonRoot, { Group: ButtonGroup });
cleanup
- run format script after make changes.
bun format, pnpm format