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
GitHub Stars
1
First Seen
Mar 1, 2026
Installed on
opencode3
gemini-cli3
github-copilot3
codex3
amp3
cline3