# Frontend

> Modern frontend development with React, Vue, and web technologies

- **Type:** Skill
- **Install:** `agentstack add skill-miles990-claude-software-skills-frontend`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [miles990](https://agentstack.voostack.com/s/miles990)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [miles990](https://github.com/miles990)
- **Source:** https://github.com/miles990/claude-software-skills/tree/main/development-stacks/frontend

## Install

```sh
agentstack add skill-miles990-claude-software-skills-frontend
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Frontend Development

## Overview

Modern frontend development patterns, frameworks, and best practices for building performant web applications.

---

## React Ecosystem

### Component Patterns

```tsx
// Functional component with hooks
import { useState, useEffect, useCallback, useMemo } from 'react';

interface UserListProps {
  initialFilter?: string;
  onSelect: (user: User) => void;
}

function UserList({ initialFilter = '', onSelect }: UserListProps) {
  const [users, setUsers] = useState([]);
  const [filter, setFilter] = useState(initialFilter);
  const [loading, setLoading] = useState(true);

  // Memoized filtered list
  const filteredUsers = useMemo(
    () => users.filter(u => u.name.toLowerCase().includes(filter.toLowerCase())),
    [users, filter]
  );

  // Stable callback reference
  const handleSelect = useCallback((user: User) => {
    onSelect(user);
  }, [onSelect]);

  useEffect(() => {
    async function fetchUsers() {
      setLoading(true);
      const data = await api.getUsers();
      setUsers(data);
      setLoading(false);
    }
    fetchUsers();
  }, []);

  if (loading) return ;

  return (
    
      
      {filteredUsers.map(user => (
         handleSelect(user)}
        />
      ))}
    
  );
}
```

### Custom Hooks

```tsx
// Data fetching hook
function useFetch(url: string) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const json = await response.json();
        setData(json);
        setError(null);
      } catch (err) {
        if (err instanceof Error && err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();
    return () => controller.abort();
  }, [url]);

  return { data, error, loading };
}

// Local storage hook
function useLocalStorage(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback((value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  }, [key, storedValue]);

  return [storedValue, setValue] as const;
}

// Debounce hook
function useDebounce(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}
```

### State Management

```tsx
// Zustand - simple global state
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  clearCart: () => void;
  total: () => number;
}

const useCartStore = create()(
  persist(
    (set, get) => ({
      items: [],
      addItem: (item) => set((state) => ({
        items: [...state.items, item]
      })),
      removeItem: (id) => set((state) => ({
        items: state.items.filter(i => i.id !== id)
      })),
      clearCart: () => set({ items: [] }),
      total: () => get().items.reduce((sum, item) => sum + item.price, 0)
    }),
    { name: 'cart-storage' }
  )
);

// React Query / TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => api.getUsers(),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newUser: CreateUserInput) => api.createUser(newUser),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}
```

---

## Next.js / App Router

### Server Components

```tsx
// app/users/page.tsx - Server Component (default)
async function UsersPage() {
  // Direct database access in server component
  const users = await db.user.findMany();

  return (
    
      Users
      
    
  );
}

// Client component for interactivity
'use client';

import { useState } from 'react';

function UserList({ users }: { users: User[] }) {
  const [selected, setSelected] = useState(null);

  return (
    
      {users.map(user => (
         setSelected(user.id)}
          className={selected === user.id ? 'selected' : ''}
        >
          {user.name}
        
      ))}
    
  );
}
```

### Server Actions

```tsx
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  await db.user.create({
    data: { name, email }
  });

  revalidatePath('/users');
  redirect('/users');
}

// Usage in component
function CreateUserForm() {
  return (
    
      
      
      Create
    
  );
}
```

### Route Handlers

```tsx
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const limit = parseInt(searchParams.get('limit') || '10');

  const users = await db.user.findMany({ take: limit });
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  const user = await db.user.create({ data: body });
  return NextResponse.json(user, { status: 201 });
}
```

---

## CSS & Styling

### Tailwind CSS

```tsx
// Component with Tailwind
function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    
      
        {title}
      
      
        {children}
      
    
  );
}

// With clsx/cn for conditional classes
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

function Button({
  variant = 'primary',
  size = 'md',
  className,
  ...props
}: ButtonProps) {
  return (
    
  );
}
```

### CSS Modules

```tsx
// Button.module.css
.button {
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
  font-weight: 500;
}

.primary {
  background-color: var(--color-primary);
  color: white;
}

.secondary {
  background-color: var(--color-gray-200);
  color: var(--color-gray-900);
}

// Button.tsx
import styles from './Button.module.css';

function Button({ variant = 'primary', children }) {
  return (
    
      {children}
    
  );
}
```

---

## Performance Optimization

### Code Splitting

```tsx
// Dynamic imports
import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => ,
  ssr: false, // Client-only component
});

// React.lazy with Suspense
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
  return (
    }>
      
    
  );
}
```

### Virtualization

```tsx
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    
      
        {virtualizer.getVirtualItems().map((virtualItem) => (
          
            {items[virtualItem.index].name}
          
        ))}
      
    
  );
}
```

### Image Optimization

```tsx
import Image from 'next/image';

function ProductImage({ src, alt }: { src: string; alt: string }) {
  return (
    
  );
}
```

---

## Testing

### Component Testing

```tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('LoginForm', () => {
  it('submits with valid credentials', async () => {
    const onSubmit = vi.fn();
    render();

    await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
    await userEvent.type(screen.getByLabelText(/password/i), 'password123');
    await userEvent.click(screen.getByRole('button', { name: /sign in/i }));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123',
      });
    });
  });

  it('shows validation errors', async () => {
    render();

    await userEvent.click(screen.getByRole('button', { name: /sign in/i }));

    expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
  });
});
```

---

## Related Skills

- [[testing-strategies]] - Frontend testing
- [[performance-optimization]] - Web performance
- [[ux-principles]] - User experience

---

## Sharp Edges（常見陷阱）

> 這些是前端開發中最常見且代價最高的錯誤

### SE-1: useEffect 無限迴圈
- **嚴重度**: critical
- **情境**: useEffect 中更新 state，而該 state 又是 dependency，導致無限重新渲染
- **原因**: 不了解 React 的 dependency 機制、object/array 作為 dependency
- **症狀**:
  - 頁面卡死或極度緩慢
  - 瀏覽器記憶體持續增長
  - Network tab 顯示大量重複請求
- **檢測**: `useEffect.*setState.*\].*\{|useEffect\(\(\).*fetch.*\[\]`
- **解法**: 正確設定 dependencies、使用 useCallback/useMemo、考慮用 useRef 追蹤值

### SE-2: 記憶體洩漏 (Memory Leak)
- **嚴重度**: high
- **情境**: Component unmount 後仍有 subscription、timer 或 async 操作更新 state
- **原因**: 沒有清理 effect、沒有取消進行中的 fetch
- **症狀**:
  - Console 警告「Can't perform state update on unmounted component」
  - 應用程式越用越慢
  - 切換頁面後舊資料閃現
- **檢測**: `useEffect.*setInterval(?!.*return)|useEffect.*addEventListener(?!.*return.*remove)|useEffect.*subscribe(?!.*return)`
- **解法**: 在 useEffect 中 return cleanup function、使用 AbortController 取消 fetch

### SE-3: Props Drilling 地獄
- **嚴重度**: medium
- **情境**: 為了傳遞資料給深層 component，中間層需要傳遞不需要的 props
- **原因**: 沒有使用 Context 或狀態管理、component 結構設計不當
- **症狀**:
  - 修改一個 prop 需要改動 5+ 個 component
  - 中間層 component 有很多只是「傳遞」的 props
  - 難以追蹤資料流向
- **檢測**: `props\.\w+.*props\.\w+.*props\.\w+|{.*,.*,.*,.*,.*,.*}.*=>`
- **解法**: 使用 Context、Zustand/Redux、或 Component Composition

### SE-4: 過早優化 (Premature Optimization)
- **嚴重度**: medium
- **情境**: 在沒有效能問題時過度使用 useMemo/useCallback/React.memo
- **原因**: 誤解這些 hooks 的用途、盲目「優化」
- **症狀**:
  - 程式碼充滿 useMemo 但沒有效能改善
  - 反而因為額外的記憶體和比較操作變慢
  - 程式碼難以閱讀
- **檢測**: `useMemo\(\(\).*return.*\d+|useCallback\(\(\).*console|React\.memo\(.*\)(?!.*areEqual)`
- **解法**: 先測量效能、只在確定有問題時優化、理解何時使用這些工具

### SE-5: 不安全的 HTML 渲染
- **嚴重度**: critical
- **情境**: 使用 dangerouslySetInnerHTML 或直接渲染用戶輸入的 HTML
- **原因**: 為了渲染 rich text、不了解 XSS 風險
- **症狀**:
  - XSS 攻擊漏洞
  - 用戶可以注入惡意腳本
  - 網站被用於釣魚或竊取資料
- **檢測**: `dangerouslySetInnerHTML|innerHTML.*=.*user|v-html.*user`
- **解法**: 使用 DOMPurify 消毒、使用安全的 Markdown 渲染器、避免直接渲染用戶輸入

---

## Validations

### V-1: useEffect 缺少 cleanup
- **類型**: regex
- **嚴重度**: high
- **模式**: `useEffect\s*\([^)]*=>\s*\{[^}]*(setInterval|addEventListener|subscribe)[^}]*\}(?![^}]*return)`
- **訊息**: useEffect with subscription/timer missing cleanup function
- **修復建議**: Add cleanup: `return () => clearInterval(id)` or `return () => unsubscribe()`
- **適用**: `*.tsx`, `*.jsx`

### V-2: 禁止 dangerouslySetInnerHTML
- **類型**: regex
- **嚴重度**: critical
- **模式**: `dangerouslySetInnerHTML`
- **訊息**: dangerouslySetInnerHTML is a security risk (XSS)
- **修復建議**: Use DOMPurify: `dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}`
- **適用**: `*.tsx`, `*.jsx`

### V-3: useEffect 空 dependency array + state 更新
- **類型**: regex
- **嚴重度**: high
- **模式**: `useEffect\s*\([^)]*=>\s*\{[^}]*set\w+\([^}]*\},\s*\[\s*\]\s*\)`
- **訊息**: useEffect with empty deps but updates state - possible stale closure
- **修復建議**: Add necessary dependencies or use useRef for values that shouldn't trigger re-run
- **適用**: `*.tsx`, `*.jsx`

### V-4: 禁止 inline styles 物件字面量
- **類型**: regex
- **嚴重度**: medium
- **模式**: `style=\{\s*\{[^}]+\}\s*\}`
- **訊息**: Inline style object creates new reference on every render
- **修復建議**: Extract to variable or use useMemo: `const styles = useMemo(() => ({...}), [])`
- **適用**: `*.tsx`, `*.jsx`

### V-5: 禁止在 JSX 中使用 index 作為 key
- **類型**: regex
- **嚴重度**: medium
- **模式**: `key=\{(index|i|idx)\}`
- **訊息**: Using array index as key can cause issues with reordering
- **修復建議**: Use a unique identifier: `key={item.id}` or `key={item.uniqueField}`
- **適用**: `*.tsx`, `*.jsx`

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [miles990](https://github.com/miles990)
- **Source:** [miles990/claude-software-skills](https://github.com/miles990/claude-software-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-miles990-claude-software-skills-frontend
- Seller: https://agentstack.voostack.com/s/miles990
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
