Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
alinaqi avatar

React Web

  • 3 installs
  • 706 repo stars
  • Updated July 14, 2026
  • alinaqi/maggy

react-web is a Claude Code skill that enforces test-first React web development with hooks, React Query, and Zustand.

About

This skill is a React web-development guide Claude applies when editing React components or pages. It enforces test-first development, functional components, and logic extracted into hooks. Developers use it to keep web UI structured and covered by tests before implementation. It also defines a standard component and page directory layout.

  • Mandates test-first development: write Component.test.tsx before implementation
  • Prescribes core/components/pages/hooks/store project structure
  • Recommends React Query for server state and Zustand for global state

React Web by the numbers

  • 3 all-time installs (skills.sh)
  • Ranked #1,841 of 2,245 Frontend Development skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
At a glance

react-web capabilities & compatibility

Free; open-source React tooling.

Capabilities
react components · test first development · state management
Use cases
frontend · testing · ui design
IDEs
vscode
Pricing
Free
From the docs

What react-web says it does

React web development with hooks, React Query, Zustand
SKILL.md
CRITICAL: Tests MUST be written BEFORE implementation code. This is non-negotiable for frontend components.
SKILL.md
npx skills add https://github.com/alinaqi/maggy --skill react-web

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs3
repo stars706
Last updatedJuly 14, 2026
Repositoryalinaqi/maggy

What it does

Apply test-first React web conventions for components, pages, and hooks when building a web UI.

Who is it for?

React web apps that want tests written before component implementation.

When should I use this skill?

Working on React web components or pages (src/components, src/pages, src/app).

What you get

React components with test-first coverage, hook-extracted logic, and a consistent directory structure.

  • test-first workflow
  • component structure
  • state management setup

By the numbers

  • 5-step TFD workflow

Files

SKILL.mdMarkdownGitHub ↗

React Web Skill

---

Test-First Development (MANDATORY)

CRITICAL: Tests MUST be written BEFORE implementation code. This is non-negotiable for frontend components.

The TFD Workflow

1. Write test file first → Defines expected behavior
2. Run test (it fails) → Confirms test is valid
3. Write minimal code → Just enough to pass
4. Run test (it passes) → Validates implementation
5. Refactor if needed → Tests catch regressions

Component Development Order

# CORRECT ORDER - Test first
1. Create Button.test.tsx    # Write tests for expected behavior
2. Run tests (they fail)     # npm test -- Button
3. Create Button.tsx         # Implement to pass tests
4. Run tests (they pass)     # Verify implementation
5. Create Button.module.css  # Style after logic works

# WRONG ORDER - Never do this
1. Create Button.tsx         # ❌ No tests exist yet
2. Create Button.module.css  # ❌ Still no tests
3. "I'll add tests later"    # ❌ Tests never get written

Test File Structure (Create First)

// Button.test.tsx - CREATE THIS FIRST
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  // Define ALL expected behaviors upfront
  describe('rendering', () => {
    it('renders with label', () => {
      render(<Button label="Click me" onClick={() => {}} />);
      expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
    });

    it('applies variant class', () => {
      render(<Button label="Click" onClick={() => {}} variant="secondary" />);
      expect(screen.getByRole('button')).toHaveClass('secondary');
    });
  });

  describe('interactions', () => {
    it('calls onClick when clicked', () => {
      const onClick = vi.fn();
      render(<Button label="Click me" onClick={onClick} />);
      fireEvent.click(screen.getByRole('button'));
      expect(onClick).toHaveBeenCalledTimes(1);
    });

    it('does not call onClick when disabled', () => {
      const onClick = vi.fn();
      render(<Button label="Click me" onClick={onClick} disabled />);
      fireEvent.click(screen.getByRole('button'));
      expect(onClick).not.toHaveBeenCalled();
    });
  });

  describe('accessibility', () => {
    it('has correct aria attributes when disabled', () => {
      render(<Button label="Click" onClick={() => {}} disabled />);
      expect(screen.getByRole('button')).toHaveAttribute('aria-disabled', 'true');
    });
  });
});

Hook Test First Pattern

// useCounter.test.ts - CREATE THIS FIRST
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('starts at initial value', () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);
  });

  it('increments', () => {
    const { result } = renderHook(() => useCounter());
    act(() => result.current.increment());
    expect(result.current.count).toBe(1);
  });

  it('decrements', () => {
    const { result } = renderHook(() => useCounter(5));
    act(() => result.current.decrement());
    expect(result.current.count).toBe(4);
  });

  it('resets to initial value', () => {
    const { result } = renderHook(() => useCounter(10));
    act(() => result.current.increment());
    act(() => result.current.reset());
    expect(result.current.count).toBe(10);
  });
});

Enforcement Checklist

Before writing ANY component/hook implementation:

  • [ ] Test file exists: Component.test.tsx
  • [ ] All expected behaviors have test cases
  • [ ] Tests run and FAIL (proves tests are valid)
  • [ ] Only THEN create implementation file

If tests are skipped, Claude MUST:

⚠️ TEST-FIRST VIOLATION

Cannot create [Component].tsx - no test file exists.

Creating [Component].test.tsx first with tests for:
- Rendering with required props
- User interactions
- Edge cases
- Accessibility

---

Project Structure

project/
├── src/
│   ├── core/                   # Pure business logic (no React)
│   │   ├── types.ts
│   │   └── services/
│   ├── components/             # Reusable UI components
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.test.tsx
│   │   │   ├── Button.module.css  # or .styles.ts
│   │   │   └── index.ts
│   │   └── index.ts            # Barrel export
│   ├── pages/                  # Route-level components
│   │   ├── Home/
│   │   │   ├── HomePage.tsx
│   │   │   ├── useHome.ts      # Page-specific hook
│   │   │   └── index.ts
│   │   └── index.ts
│   ├── hooks/                  # Shared custom hooks
│   ├── store/                  # State management
│   ├── api/                    # API client and queries
│   ├── utils/                  # Utilities
│   ├── App.tsx
│   └── main.tsx
├── tests/
│   ├── unit/
│   └── e2e/
├── public/
├── package.json
├── tsconfig.json
├── vite.config.ts              # or next.config.js
└── CLAUDE.md

---

Component Patterns

Functional Components Only

// Good - simple, testable
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary';
}

export function Button({
  label,
  onClick,
  disabled = false,
  variant = 'primary'
}: ButtonProps): JSX.Element {
  return (
    <button
      className={styles[variant]}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

Extract Logic to Hooks

// useHome.ts - all logic here
export function useHome() {
  const [items, setItems] = useState<Item[]>([]);
  const [loading, setLoading] = useState(false);

  const refresh = useCallback(async () => {
    setLoading(true);
    const data = await fetchItems();
    setItems(data);
    setLoading(false);
  }, []);

  useEffect(() => {
    refresh();
  }, [refresh]);

  return { items, loading, refresh };
}

// HomePage.tsx - pure presentation
export function HomePage(): JSX.Element {
  const { items, loading, refresh } = useHome();

  if (loading) return <Spinner />;

  return <ItemList items={items} onRefresh={refresh} />;
}

Props Interface Always Explicit

// Always define props interface, even if simple
interface ItemCardProps {
  item: Item;
  onClick: (id: string) => void;
}

export function ItemCard({ item, onClick }: ItemCardProps): JSX.Element {
  return (
    <div onClick={() => onClick(item.id)}>
      <h3>{item.title}</h3>
    </div>
  );
}

---

State Management

Local State First

// Start with useState, escalate only when needed
const [value, setValue] = useState('');

Zustand for Global State (if needed)

// store/useAppStore.ts
import { create } from 'zustand';

interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  setUser: (user: User | null) => void;
  toggleTheme: () => void;
}

export const useAppStore = create<AppState>((set) => ({
  user: null,
  theme: 'light',
  setUser: (user) => set({ user }),
  toggleTheme: () => set((state) => ({
    theme: state.theme === 'light' ? 'dark' : 'light'
  })),
}));

React Query for Server State

// api/queries/useItems.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { itemsApi } from '../client';

export function useItems() {
  return useQuery({
    queryKey: ['items'],
    queryFn: itemsApi.getAll,
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

export function useCreateItem() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: itemsApi.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['items'] });
    },
  });
}

---

Routing

React Router (Vite/CRA)

// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';

export function App(): JSX.Element {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/items/:id" element={<ItemPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  );
}

Protected Routes

interface ProtectedRouteProps {
  children: JSX.Element;
}

function ProtectedRoute({ children }: ProtectedRouteProps): JSX.Element {
  const { user } = useAppStore();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

---

Styling

CSS Modules (Preferred)

// Button.module.css
.primary {
  background: var(--color-primary);
  color: white;
}

.secondary {
  background: transparent;
  border: 1px solid var(--color-primary);
}

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

<button className={styles.primary}>Click</button>

Tailwind (Alternative)

// Use consistent patterns, extract repeated combinations
const buttonVariants = {
  primary: 'bg-blue-500 text-white hover:bg-blue-600',
  secondary: 'bg-transparent border border-blue-500 text-blue-500',
} as const;

<button className={buttonVariants[variant]}>{label}</button>

---

Forms

React Hook Form + Zod

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

type FormData = z.infer<typeof schema>;

export function LoginForm(): JSX.Element {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(schema),
  });

  const onSubmit = (data: FormData) => {
    // handle submit
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="password" {...register('password')} />
      {errors.password && <span>{errors.password.message}</span>}

      <button type="submit">Login</button>
    </form>
  );
}

---

Testing

Component Testing with React Testing Library

import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('calls onClick when clicked', () => {
    const onClick = vi.fn();
    render(<Button label="Click me" onClick={onClick} />);

    fireEvent.click(screen.getByText('Click me'));

    expect(onClick).toHaveBeenCalledTimes(1);
  });

  it('does not call onClick when disabled', () => {
    const onClick = vi.fn();
    render(<Button label="Click me" onClick={onClick} disabled />);

    fireEvent.click(screen.getByText('Click me'));

    expect(onClick).not.toHaveBeenCalled();
  });

  it('applies correct variant class', () => {
    render(<Button label="Click" onClick={() => {}} variant="secondary" />);

    expect(screen.getByRole('button')).toHaveClass('secondary');
  });
});

Hook Testing

import { renderHook, act, waitFor } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('increments counter', () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });
});

E2E with Playwright

// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test('user can login', async ({ page }) => {
  await page.goto('/login');

  await page.fill('[name="email"]', 'test@example.com');
  await page.fill('[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Welcome')).toBeVisible();
});

---

Performance

Memoization

// Memoize expensive components
const ItemList = memo(function ItemList({ items }: ItemListProps) {
  return items.map(item => <ItemCard key={item.id} item={item} />);
});

// Memoize callbacks passed to children
const handleClick = useCallback((id: string) => {
  setSelectedId(id);
}, []);

// Memoize expensive computations
const sortedItems = useMemo(() => {
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

Code Splitting

// Lazy load routes
const ItemPage = lazy(() => import('./pages/Item'));

<Suspense fallback={<Spinner />}>
  <Route path="/items/:id" element={<ItemPage />} />
</Suspense>

---

React Web Anti-Patterns

  • ❌ Inline functions in JSX - use useCallback
  • ❌ Logic in render - extract to hooks
  • ❌ Deep component nesting - flatten hierarchy
  • ❌ Index as key in lists - use stable IDs
  • ❌ Direct state mutation - always use setter
  • ❌ Prop drilling > 2 levels - use context or state management
  • ❌ useEffect for derived state - use useMemo
  • ❌ Fetching in useEffect - use React Query
  • ❌ Mixing business logic with UI - keep core/ pure
  • ❌ Large components (>100 lines) - split into smaller pieces
  • ❌ CSS in JS objects - use CSS modules or Tailwind
  • ❌ Ignoring TypeScript errors - fix them

Related skills

FAQ

Does this skill require tests before code?

Yes. It mandates writing the test file first, running it to fail, then implementing to pass.

What state tools does it recommend?

React Query for server state and Zustand for global state, with useState for local state.

Frontend Developmentfrontendtesting

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.