frontend-component by aiskillstore
Next.js 16+ uses App Router with Server Components by default. Client Components are only used when interactivity is needed (hooks, event handlers, browser APIs).
Content & Writing
85 Stars
2 Forks
Updated Jan 19, 2026, 04:39 AM
Why Use This
This skill provides specialized capabilities for aiskillstore's codebase.
Use Cases
- Developing new features in the aiskillstore repository
- Refactoring existing code to follow aiskillstore standards
- Understanding and working with aiskillstore's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 928 Lines
Total Files 1
Total Size 0 B
License NOASSERTION
---
name: frontend-component
description: Next.js 16+ uses App Router with Server Components by default. Client Components are only used when interactivity is needed (hooks, event handlers, browser APIs).
---
# Frontend Component Skill
**Purpose**: Guidance for creating Next.js components following server/client patterns and existing component structures.
## Overview
Next.js 16+ uses App Router with Server Components by default. Client Components are only used when interactivity is needed (hooks, event handlers, browser APIs).
## Server vs Client Components
### Server Components (Default)
**When to Use**:
- Pages and layouts
- Static content
- Data fetching from API (when possible)
- SEO-optimized content
**Pattern**:
```typescript
// No "use client" directive
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Page Title",
};
export default function PageComponent() {
return <div>Static content</div>;
}
```
**Example**: `frontend/app/layout.tsx`, `frontend/app/page.tsx`
### Client Components (When Needed)
**When to Use**:
- Interactive elements (buttons, forms, inputs)
- Event handlers (onClick, onChange, etc.)
- React hooks (useState, useEffect, useRouter, etc.)
- Browser APIs (localStorage, window, document, etc.)
- Real-time updates
- Drag and drop functionality
**Pattern**:
```typescript
"use client"; // MUST be first line
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
interface ComponentProps {
prop1: string;
prop2?: number;
}
export default function ComponentName({ prop1, prop2 }: ComponentProps) {
const router = useRouter();
const [state, setState] = useState("");
useEffect(() => {
// Side effects
}, []);
return <div>{/* Component JSX */}</div>;
}
```
**Example**: `frontend/components/ProtectedRoute.tsx`, `frontend/app/signup/page.tsx`
## Component Structure Template
```typescript
"use client"; // Only if client component
/**
* Component Name
*
* Brief description of what this component does
*/
import { useState, useEffect } from "react";
import { ComponentType } from "@/types";
import { cn } from "@/lib/utils";
interface ComponentProps {
prop1: string;
prop2?: number;
className?: string;
}
export default function ComponentName({ prop1, prop2, className }: ComponentProps) {
// State
const [state, setState] = useState("");
// Effects
useEffect(() => {
// Side effects
}, []);
// Handlers
const handleClick = () => {
// Handler logic
};
// Render
return (
<div className={cn("base-classes", className)}>
{/* Component content */}
</div>
);
}
```
## Specific Component Patterns
### 1. ProtectedRoute Pattern
**From**: `frontend/components/ProtectedRoute.tsx`
```typescript
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { isAuthenticated } from "@/lib/auth";
import LoadingSpinner from "./LoadingSpinner";
interface ProtectedRouteProps {
children: React.ReactNode;
}
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const router = useRouter();
const [isAuthorized, setIsAuthorized] = useState(false);
const [isChecking, setIsChecking] = useState(true);
useEffect(() => {
async function checkAuth() {
try {
const authenticated = await isAuthenticated();
if (!authenticated) {
const currentPath = window.location.pathname;
if (currentPath !== "/signin") {
sessionStorage.setItem("redirectAfterLogin", currentPath);
}
router.push("/signin");
} else {
setIsAuthorized(true);
}
} catch (error) {
console.error("Auth check failed:", error);
router.push("/signin");
} finally {
setIsChecking(false);
}
}
checkAuth();
}, [router]);
if (isChecking) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner size="large" />
</div>
);
}
if (!isAuthorized) {
return null;
}
return <>{children}</>;
}
```
**Pattern**:
- Check authentication on mount
- Show loading spinner during check
- Store intended destination in sessionStorage
- Redirect to `/signin` if not authenticated
- Only render children if authorized
### 2. LoadingSpinner Pattern
**From**: `frontend/components/LoadingSpinner.tsx`
```typescript
interface LoadingSpinnerProps {
size?: "small" | "medium" | "large";
color?: string;
label?: string;
}
export default function LoadingSpinner({
size = "medium",
color = "blue",
label = "Loading...",
}: LoadingSpinnerProps) {
const sizeClasses = {
small: "w-4 h-4 border-2",
medium: "w-8 h-8 border-3",
large: "w-12 h-12 border-4",
};
const colorClasses = {
blue: "border-blue-600 border-t-transparent",
gray: "border-gray-600 border-t-transparent",
white: "border-white border-t-transparent",
};
const spinnerClass = `${sizeClasses[size]} ${
colorClasses[color as keyof typeof colorClasses] || colorClasses.blue
} rounded-full animate-spin`;
return (
<div
className="flex items-center justify-center"
role="status"
aria-label={label}
aria-live="polite"
>
<div className={spinnerClass}></div>
<span className="sr-only">{label}</span>
</div>
);
}
```
**Pattern**:
- Multiple sizes (small, medium, large)
- Multiple colors (blue, gray, white)
- Accessibility labels (`role="status"`, `aria-label`, `aria-live`)
- Screen reader text with `sr-only` class
### 3. Form Handling Pattern
**From**: `frontend/app/signup/page.tsx`
```typescript
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api";
import { isValidEmail, getPasswordStrength } from "@/lib/utils";
export default function SignupPage() {
const router = useRouter();
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState("");
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = "Name is required";
}
if (!formData.email.trim()) {
newErrors.email = "Email is required";
} else if (!isValidEmail(formData.email)) {
newErrors.email = "Please enter a valid email address";
}
// More validation...
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setApiError("");
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
const response = await api.signup(formData);
if (response.success) {
router.push("/dashboard");
} else {
setApiError(response.message || "Signup failed");
}
} catch (error: any) {
setApiError(error.message || "An error occurred");
} finally {
setIsLoading(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Clear error for this field when user starts typing
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: "" }));
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
);
}
```
**Pattern**:
- Separate state for form data, errors, loading, API errors
- Validation function that returns boolean
- Clear errors on input change
- Loading state during submission
- Try-catch for error handling
- Redirect on success
### 4. ToastNotification Pattern
**From**: `frontend/components/ToastNotification.tsx`
```typescript
"use client";
import { useEffect, useState } from "react";
import { ToastMessage, ToastType } from "@/types";
export function useToast() {
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const showToast = (type: ToastType, message: string, duration?: number) => {
const id = `toast-${Date.now()}-${Math.random()}`;
const newToast: ToastMessage = {
id,
type,
message,
duration,
};
setToasts((prev) => [...prev, newToast]);
};
const dismissToast = (id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
};
return {
toasts,
showToast,
dismissToast,
success: (message: string, duration?: number) => showToast("success", message, duration),
error: (message: string, duration?: number) => showToast("error", message, duration),
// ...
};
}
```
**Pattern**:
- Custom hook for toast management
- Auto-dismiss with duration
- Stack multiple toasts
- Helper methods (success, error, warning, info)
## Tailwind CSS Patterns
### 1. Utility Classes Only
```typescript
<div className="flex items-center justify-center gap-4 p-6 bg-white rounded-lg shadow-md">
```
**Pattern**: Use Tailwind utility classes, no inline styles
### 2. Conditional Classes with `cn()` Utility
```typescript
import { cn } from "@/lib/utils";
<div className={cn(
"base-classes",
condition && "conditional-classes",
className // Allow prop override
)}>
```
**Pattern**: Use `cn()` from `@/lib/utils` for conditional classes
### 3. Dark Mode Support
```typescript
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
```
**Pattern**: Use `dark:` prefix for dark mode styles
### 4. Responsive Design
```typescript
<div className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4">
```
**Pattern**: Use breakpoint prefixes (`sm:`, `md:`, `lg:`, `xl:`)
## Accessibility Patterns (WCAG 2.1 AA)
### 1. ARIA Labels
```typescript
<button aria-label="Close dialog">×</button>
<div role="status" aria-live="polite" aria-label="Loading...">
```
**Pattern**: Always provide `aria-label` for icon-only buttons
### 2. Semantic HTML
```typescript
<nav>
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
```
**Pattern**: Use semantic HTML elements (`nav`, `main`, `section`, `article`, etc.)
### 3. Keyboard Navigation
```typescript
<button
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleClick();
}
}}
>
```
**Pattern**: Ensure keyboard accessibility for all interactive elements
### 4. Screen Reader Text
```typescript
<span className="sr-only">Loading content</span>
```
**Pattern**: Use `sr-only` class for screen reader-only text
### 5. Focus Management
```typescript
<input
autoFocus
className="focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
```
**Pattern**: Always provide visible focus indicators
## File Naming Conventions
- **Pages**: kebab-case (e.g., `signup.tsx`, `signin.tsx`)
- **Components**: PascalCase (e.g., `TaskList.tsx`, `TaskItem.tsx`)
- **Layouts**: `layout.tsx`
- **Error Pages**: `error.tsx`, `not-found.tsx`
## Constitution Requirements
- **FR-033**: Next.js 16+ App Router structure ✅
- **FR-034**: Server components default, client when needed ✅
- **FR-035**: Error boundaries ✅
- **FR-036**: WCAG 2.1 AA compliance ✅
- **FR-037**: TypeScript strict mode ✅
- **FR-038**: Prettier formatting ✅
- **FR-039**: ESLint rules ✅
## References
- **Specification**: `specs/002-frontend-todo-app/spec.md` - Component specifications
- **Existing Components**: `frontend/components/*.tsx` - Component examples
- **Existing Pages**: `frontend/app/*.tsx` - Page examples
## Advanced Component Patterns
### 1. Drag and Drop Pattern (Phase 7 - T065)
**Library**: `@dnd-kit/core`
```typescript
"use client";
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
export default function SortableTaskList({ tasks, onReorder }: Props) {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
onReorder(active.id, over.id);
}
};
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
{tasks.map(task => (
<SortableTaskItem key={task.id} task={task} />
))}
</SortableContext>
</DndContext>
);
}
```
**Pattern**: Use `@dnd-kit/core` for drag and drop, handle reorder on drag end
### 2. Undo/Redo Pattern (Phase 7 - T066)
```typescript
"use client";
import { useReducer } from "react";
interface HistoryState<T> {
past: T[];
present: T;
future: T[];
}
function historyReducer<T>(state: HistoryState<T>, action: { type: string; newPresent?: T }): HistoryState<T> {
const { past, present, future } = state;
switch (action.type) {
case "UNDO":
if (past.length === 0) return state;
return {
past: past.slice(0, past.length - 1),
present: past[past.length - 1],
future: [present, ...future],
};
case "REDO":
if (future.length === 0) return state;
return {
past: [...past, present],
present: future[0],
future: future.slice(1),
};
case "SET":
if (action.newPresent === present) return state;
return {
past: [...past, present],
present: action.newPresent!,
future: [],
};
default:
return state;
}
}
export function useHistory<T>(initialPresent: T) {
const [state, dispatch] = useReducer(historyReducer, {
past: [],
present: initialPresent,
future: [],
});
const undo = () => dispatch({ type: "UNDO" });
const redo = () => dispatch({ type: "REDO" });
const set = (newPresent: T) => dispatch({ type: "SET", newPresent });
return { state: state.present, set, undo, redo, canUndo: state.past.length > 0, canRedo: state.future.length > 0 };
}
```
**Pattern**: Use `useReducer` with history pattern for undo/redo functionality
### 3. Real-time Updates with Polling (Phase 7 - T067)
```typescript
"use client";
import { useEffect, useRef } from "react";
export function usePolling(callback: () => Promise<void>, interval: number = 5000) {
const intervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const poll = async () => {
try {
await callback();
} catch (error) {
console.error("Polling error:", error);
}
};
// Initial call
poll();
// Set up polling interval
intervalRef.current = setInterval(poll, interval);
// Cleanup
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [callback, interval]);
}
```
**Pattern**: Use `setInterval` for polling, cleanup on unmount, handle errors gracefully
### 4. Inline Editing Pattern (Phase 7 - T068)
```typescript
"use client";
import { useState } from "react";
export default function InlineEditable({ value, onSave }: Props) {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const handleSave = () => {
onSave(editValue);
setIsEditing(false);
};
const handleCancel = () => {
setEditValue(value);
setIsEditing(false);
};
if (isEditing) {
return (
<input
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={handleSave}
onKeyDown={(e) => {
if (e.key === "Enter") handleSave();
if (e.key === "Escape") handleCancel();
}}
autoFocus
/>
);
}
return (
<span onClick={() => setIsEditing(true)} className="cursor-pointer">
{value}
</span>
);
}
```
**Pattern**: Toggle edit mode, save on blur/Enter, cancel on Escape
### 5. Performance Optimization Patterns (Phase 8 - T072)
#### Code Splitting with `next/dynamic`
```typescript
import dynamic from "next/dynamic";
// Lazy load heavy components
const TaskStatistics = dynamic(() => import("@/components/TaskStatistics"), {
loading: () => <LoadingSpinner />,
ssr: false, // Disable SSR if not needed
});
const TaskDetailModal = dynamic(() => import("@/components/TaskDetailModal"), {
loading: () => <LoadingSpinner />,
});
```
**Pattern**: Use `next/dynamic` for code splitting, provide loading fallback
#### Image Optimization
```typescript
import Image from "next/image";
<Image
src="/image.jpg"
alt="Description"
width={500}
height={300}
loading="lazy"
placeholder="blur"
/>
```
**Pattern**: Use Next.js `Image` component for automatic optimization
### 6. Error Boundary Pattern (Phase 8 - T074)
```typescript
"use client";
import React, { Component, ErrorInfo, ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("ErrorBoundary caught an error:", error, errorInfo);
// Log to error tracking service
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<h2 className="text-red-800 font-bold">Something went wrong</h2>
<p className="text-red-600">{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>Try again</button>
</div>
)
);
}
return this.props.children;
}
}
```
**Pattern**: Class component, catch errors, provide fallback UI, log errors
### 7. PWA Patterns (Phase 8 - T069, T070, T071)
#### Service Worker Setup
```typescript
// public/sw.js or use next-pwa
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("SW registered:", registration);
})
.catch((error) => {
console.error("SW registration failed:", error);
});
});
}
```
**Pattern**: Register service worker on page load, handle registration errors
#### IndexedDB for Offline Storage
```typescript
import { openDB, DBSchema, IDBPDatabase } from "idb";
interface TaskDB extends DBSchema {
tasks: {
key: number;
value: Task;
indexes: { "by-user-id": string };
};
}
export async function getDB(): Promise<IDBPDatabase<TaskDB>> {
return openDB<TaskDB>("todo-db", 1, {
upgrade(db) {
const taskStore = db.createObjectStore("tasks", { keyPath: "id" });
taskStore.createIndex("by-user-id", "user_id");
},
});
}
export async function saveTaskOffline(task: Task) {
const db = await getDB();
await db.put("tasks", task);
}
export async function getTasksOffline(userId: string): Promise<Task[]> {
const db = await getDB();
return db.getAllFromIndex("tasks", "by-user-id", userId);
}
```
**Pattern**: Use `idb` library for IndexedDB, create stores and indexes, handle offline data
#### Offline Sync Mechanism
```typescript
export async function syncOfflineChanges(userId: string) {
const db = await getDB();
const offlineTasks = await db.getAllFromIndex("tasks", "by-user-id", userId);
for (const task of offlineTasks) {
if (task.syncStatus === "pending") {
try {
await api.createTask(userId, task);
await db.put("tasks", { ...task, syncStatus: "synced" });
} catch (error) {
console.error("Sync failed for task:", task.id, error);
}
}
}
}
// Call on connection restore
window.addEventListener("online", () => {
syncOfflineChanges(currentUserId);
});
```
**Pattern**: Track sync status, sync on connection restore, handle sync errors
### 8. Caching Strategies (Phase 8 - T073)
```typescript
// Cache API responses
const cache = new Map<string, { data: any; timestamp: number }>();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
export async function getCachedData<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
const data = await fetcher();
cache.set(key, { data, timestamp: Date.now() });
return data;
}
```
**Pattern**: Use Map for in-memory cache, check expiration, update cache on fetch
### 9. Error Logging and Tracking (Phase 8 - T075)
```typescript
export function logError(error: Error, context?: Record<string, any>) {
console.error("Error:", error, context);
// Send to error tracking service (e.g., Sentry)
if (typeof window !== "undefined" && (window as any).Sentry) {
(window as any).Sentry.captureException(error, {
extra: context,
});
}
// Or send to custom endpoint
fetch("/api/log-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString(),
}),
}).catch((err) => console.error("Failed to log error:", err));
}
```
**Pattern**: Log to console, send to error tracking service, include context
## Common Patterns Summary
1. ✅ Use Server Components by default
2. ✅ Add `"use client"` only when needed
3. ✅ Use TypeScript interfaces for props
4. ✅ Use `cn()` utility for conditional classes
5. ✅ Always include accessibility attributes
6. ✅ Use semantic HTML elements
7. ✅ Provide loading states
8. ✅ Handle errors gracefully
9. ✅ Use Tailwind utility classes
10. ✅ Support dark mode with `dark:` prefix
11. ✅ Use `@dnd-kit/core` for drag and drop
12. ✅ Use `useReducer` for undo/redo
13. ✅ Use `setInterval` for polling with cleanup
14. ✅ Use `next/dynamic` for code splitting
15. ✅ Use ErrorBoundary for error handling
16. ✅ Use IndexedDB for offline storage
17. ✅ Sync offline changes on connection restore
18. ✅ Cache API responses with expiration
19. ✅ Log errors to tracking service
Name Size