frontend-modern
SKILL.md
Modern Frontend Development Patterns
React Component Patterns
Functional Components
import { useState, useEffect } from 'react';
interface CardProps {
title: string;
description: string;
imageUrl?: string;
onClick?: () => void;
}
export const Card = ({ title, description, imageUrl, onClick }: CardProps) => {
return (
<article className="card" onClick={onClick}>
{imageUrl && (
<img src={imageUrl} alt={title} className="card__image" />
)}
<div className="card__content">
<h3 className="card__title">{title}</h3>
<p className="card__description">{description}</p>
</div>
</article>
);
};
Component with Children
interface ContainerProps {
children: React.ReactNode;
className?: string;
as?: keyof JSX.IntrinsicElements;
}
export const Container = ({
children,
className = '',
as: Component = 'div'
}: ContainerProps) => {
return (
<Component className={`container ${className}`}>
{children}
</Component>
);
};
Compound Components
interface AccordionContextType {
openItems: string[];
toggle: (id: string) => void;
}
const AccordionContext = createContext<AccordionContextType | null>(null);
export const Accordion = ({ children }: { children: React.ReactNode }) => {
const [openItems, setOpenItems] = useState<string[]>([]);
const toggle = (id: string) => {
setOpenItems(prev =>
prev.includes(id)
? prev.filter(item => item !== id)
: [...prev, id]
);
};
return (
<AccordionContext.Provider value={{ openItems, toggle }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
};
Accordion.Item = ({ id, title, children }: {
id: string;
title: string;
children: React.ReactNode;
}) => {
const context = useContext(AccordionContext);
if (!context) throw new Error('Accordion.Item must be used within Accordion');
const isOpen = context.openItems.includes(id);
return (
<div className="accordion__item">
<button
className="accordion__trigger"
onClick={() => context.toggle(id)}
aria-expanded={isOpen}
>
{title}
</button>
{isOpen && (
<div className="accordion__content">{children}</div>
)}
</div>
);
};
React Hooks
useState
// Simple state
const [count, setCount] = useState(0);
// Object state
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
// Update object state immutably
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Functional update (when new state depends on previous)
setCount(prev => prev + 1);
useEffect
// Run on mount only
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);
// Run when dependency changes
useEffect(() => {
fetchData(userId);
}, [userId]);
// Cleanup pattern
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData);
return () => controller.abort();
}, []);
useMemo and useCallback
// Memoize expensive computation
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// Memoize callback for child components
const handleClick = useCallback((id: string) => {
setSelectedId(id);
}, []);
// Pass to optimized child
<List items={items} onItemClick={handleClick} />
Custom Hooks
// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
// useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// useFetch
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
Next.js Patterns
Page Components (App Router)
// app/products/page.tsx
import { getProducts } from '@/lib/api';
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Products</h1>
<ProductList products={products} />
</main>
);
}
// Metadata
export const metadata = {
title: 'Products',
description: 'Browse our product catalog',
};
Dynamic Routes
// app/products/[slug]/page.tsx
import { getProduct, getProducts } from '@/lib/api';
import { notFound } from 'next/navigation';
interface PageProps {
params: { slug: string };
}
export default async function ProductPage({ params }: PageProps) {
const product = await getProduct(params.slug);
if (!product) {
notFound();
}
return <ProductDetail product={product} />;
}
// Generate static params at build time
export async function generateStaticParams() {
const products = await getProducts();
return products.map(product => ({ slug: product.slug }));
}
// Dynamic metadata
export async function generateMetadata({ params }: PageProps) {
const product = await getProduct(params.slug);
return {
title: product?.name ?? 'Product Not Found',
description: product?.description,
};
}
Server vs Client Components
// Server Component (default) - can fetch data directly
// app/components/ProductList.tsx
import { getProducts } from '@/lib/api';
export async function ProductList() {
const products = await getProducts(); // Direct data fetch
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
// Client Component - for interactivity
// app/components/AddToCartButton.tsx
'use client';
import { useState } from 'react';
export function AddToCartButton({ productId }: { productId: string }) {
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
setIsLoading(true);
await addToCart(productId);
setIsLoading(false);
};
return (
<button onClick={handleClick} disabled={isLoading}>
{isLoading ? 'Adding...' : 'Add to Cart'}
</button>
);
}
API Routes
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const category = searchParams.get('category');
const products = await db.products.findMany({
where: category ? { category } : undefined,
});
return NextResponse.json(products);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const product = await db.products.create({
data: body,
});
return NextResponse.json(product, { status: 201 });
}
Vue.js Patterns
Composition API
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
interface Product {
id: string;
name: string;
price: number;
}
const props = defineProps<{
initialProducts: Product[];
}>();
const emit = defineEmits<{
(e: 'select', product: Product): void;
}>();
const products = ref<Product[]>(props.initialProducts);
const searchQuery = ref('');
const filteredProducts = computed(() => {
return products.value.filter(p =>
p.name.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});
const handleSelect = (product: Product) => {
emit('select', product);
};
onMounted(() => {
console.log('Component mounted');
});
</script>
<template>
<div class="product-list">
<input v-model="searchQuery" placeholder="Search products..." />
<ul>
<li
v-for="product in filteredProducts"
:key="product.id"
@click="handleSelect(product)"
>
{{ product.name }} - ${{ product.price }}
</li>
</ul>
</div>
</template>
Composables
// composables/useProducts.ts
import { ref, onMounted } from 'vue';
export function useProducts() {
const products = ref<Product[]>([]);
const loading = ref(true);
const error = ref<Error | null>(null);
const fetchProducts = async () => {
loading.value = true;
try {
const response = await fetch('/api/products');
products.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
};
onMounted(fetchProducts);
return {
products,
loading,
error,
refetch: fetchProducts,
};
}
TypeScript Patterns
Interface vs Type
// Interfaces - extensible, good for objects
interface User {
id: string;
name: string;
email: string;
}
interface Admin extends User {
role: 'admin';
permissions: string[];
}
// Types - for unions, intersections, primitives
type Status = 'pending' | 'active' | 'completed';
type ID = string | number;
type UserWithStatus = User & { status: Status };
Generics
// Generic function
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}
// Generic interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Generic component props
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
Utility Types
// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit - exclude properties
type UserWithoutEmail = Omit<User, 'email'>;
// Partial - make all properties optional
type UserUpdate = Partial<User>;
// Required - make all properties required
type RequiredUser = Required<User>;
// Record - object with specific key/value types
type UserMap = Record<string, User>;
// Readonly - make all properties readonly
type ImmutableUser = Readonly<User>;
Modern CSS
CSS Modules
// styles/Button.module.css
.button {
padding: 0.5rem 1rem;
border-radius: 4px;
}
.primary {
background: var(--color-primary);
color: white;
}
.secondary {
background: transparent;
border: 1px solid var(--color-primary);
}
// Button.tsx
import styles from './Button.module.css';
export const Button = ({ variant = 'primary', children }) => (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
Tailwind CSS
// Utility classes
export const Card = ({ title, children }) => (
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
<h3 className="text-xl font-semibold text-gray-900 mb-4">{title}</h3>
<div className="text-gray-600">{children}</div>
</div>
);
// With @apply in CSS
/* styles.css */
.btn {
@apply px-4 py-2 rounded font-medium transition-colors;
}
.btn-primary {
@apply bg-blue-600 text-white hover:bg-blue-700;
}
CSS Variables
:root {
/* Colors */
--color-primary: #0066cc;
--color-primary-dark: #004999;
--color-secondary: #6c757d;
/* Typography */
--font-family-base: 'Inter', sans-serif;
--font-size-base: 1rem;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 2rem;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #4da6ff;
--color-background: #1a1a1a;
--color-text: #ffffff;
}
}
State Management
React Context
// context/ThemeContext.tsx
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}
Zustand (Lightweight State)
import { create } from 'zustand';
interface CartState {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
total: () => number;
}
export const useCart = create<CartState>((set, get) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
clearCart: () => set({ items: [] }),
total: () => get().items.reduce((sum, item) => sum + item.price, 0),
}));
Data Fetching
React Query / TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetch query
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(res => res.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
// Mutation with cache invalidation
function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newProduct: CreateProductInput) =>
fetch('/api/products', {
method: 'POST',
body: JSON.stringify(newProduct),
}).then(res => res.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
SWR
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(res => res.json());
function useProducts() {
const { data, error, isLoading, mutate } = useSWR('/api/products', fetcher, {
revalidateOnFocus: true,
refreshInterval: 30000,
});
return {
products: data,
isLoading,
isError: error,
refresh: mutate,
};
}
Error Handling
Error Boundaries
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught:', error, errorInfo);
// Send to error tracking service
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-container">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
Async Error Handling
// With try-catch
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
console.error('Fetch failed:', error.message);
}
throw error;
}
}
// Result pattern
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function safeFetch<T>(url: string): Promise<Result<T>> {
try {
const response = await fetch(url);
if (!response.ok) {
return { success: false, error: new Error(`HTTP ${response.status}`) };
}
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: error as Error };
}
}
Weekly Installs
3
Repository
twofoldtech-dak…ketplaceGitHub Stars
1
First Seen
Mar 1, 2026
Security Audits
Installed on
opencode3
gemini-cli3
github-copilot3
codex3
amp3
cline3