Claude
Skills
Sign in
Back

react-vite

Included with Lifetime
$97 forever

React 18+ with Vite patterns - use for Mini App frontend development, component structure, hooks, and TypeScript setup

Design

What this skill does


# React + Vite Patterns for Telegram Mini App

## Project Architecture

```
mini-app/
├── public/
│   └── favicon.ico
├── src/
│   ├── components/           # Reusable UI components
│   │   ├── common/           # Generic UI (Button, Card, Modal, etc.)
│   │   ├── layout/           # Layout components (AppLayout, Navigation)
│   │   └── features/         # Feature-specific components
│   │       ├── chat/         # Chat selector, chat card
│   │       ├── settings/     # Settings toggles, forms
│   │       ├── blocklist/    # Blocklist CRUD components
│   │       └── locks/        # Lock toggles, categories
│   ├── hooks/                # Custom React hooks
│   │   ├── api/              # Data fetching hooks
│   │   ├── telegram/         # Telegram-specific hooks
│   │   └── ui/               # UI state hooks
│   ├── pages/                # Route pages (one per route)
│   ├── services/             # API client and external services
│   ├── stores/               # Zustand stores (global state)
│   ├── types/                # TypeScript type definitions
│   ├── utils/                # Helper functions
│   ├── constants/            # App constants, enums
│   ├── App.tsx               # App root with providers
│   ├── main.tsx              # Entry point
│   ├── mockEnv.ts            # Mock Telegram environment
│   └── vite-env.d.ts         # Vite type declarations
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── .env.example
```

## Layer Responsibilities

### 1. Pages (Route Level)
- One file per route
- Compose feature components
- Handle route params
- Connect to stores if needed

### 2. Components
- **common/**: Generic, reusable across features
- **layout/**: App shell, navigation
- **features/**: Feature-specific, can use hooks

### 3. Hooks
- **api/**: `useQuery`-like data fetching
- **telegram/**: `useTelegramAuth`, `useMainButton`
- **ui/**: `useModal`, `useToast`

### 4. Stores (Zustand)
- Global state only (selected chat, user)
- Feature state stays in components

### 5. Services
- API client (single instance)
- Type-safe request/response

---

## Component Patterns

### Base Component Template

```tsx
// src/components/features/chat/ChatCard.tsx
import { memo } from 'react';
import { Cell } from '@telegram-apps/ui';
import type { Chat } from '@/types';
import styles from './ChatCard.module.css';

interface ChatCardProps {
  chat: Chat;
  isActive?: boolean;
  onSelect: (chatId: number) => void;
}

export const ChatCard = memo(function ChatCard({
  chat,
  isActive = false,
  onSelect,
}: ChatCardProps) {
  return (
    <Cell
      className={isActive ? styles.active : undefined}
      onClick={() => onSelect(chat.id)}
      subtitle={`${chat.memberCount} members`}
    >
      {chat.title}
    </Cell>
  );
});
```

### Page Component Pattern

```tsx
// src/pages/SettingsPage.tsx
import { useParams, Navigate } from 'react-router-dom';
import { Section, Spinner, Placeholder } from '@telegram-apps/ui';
import { useSettings } from '@/hooks/api/useSettings';
import { SettingsForm } from '@/components/features/settings/SettingsForm';
import { useSelectedChat } from '@/stores/chatStore';

export function SettingsPage() {
  const { chatId } = useParams<{ chatId: string }>();
  const numericChatId = Number(chatId);

  const { data: settings, isLoading, error } = useSettings(numericChatId);

  if (!chatId || isNaN(numericChatId)) {
    return <Navigate to="/" replace />;
  }

  if (isLoading) {
    return <Spinner size="large" />;
  }

  if (error || !settings) {
    return (
      <Placeholder
        header="Error"
        description={error?.message || 'Failed to load settings'}
      />
    );
  }

  return (
    <Section header="Chat Settings">
      <SettingsForm settings={settings} chatId={numericChatId} />
    </Section>
  );
}
```

### Compound Component Pattern (For Complex UI)

```tsx
// src/components/features/locks/LockGrid.tsx
import { createContext, useContext, ReactNode } from 'react';
import type { LockType, LockCategory } from '@/types';

interface LockGridContextValue {
  lockedTypes: Set<LockType>;
  onToggle: (type: LockType) => void;
}

const LockGridContext = createContext<LockGridContextValue | null>(null);

function useLockGridContext() {
  const ctx = useContext(LockGridContext);
  if (!ctx) throw new Error('LockGrid.* must be used within LockGrid');
  return ctx;
}

// Root component
interface LockGridProps {
  lockedTypes: Set<LockType>;
  onToggle: (type: LockType) => void;
  children: ReactNode;
}

function LockGridRoot({ lockedTypes, onToggle, children }: LockGridProps) {
  return (
    <LockGridContext.Provider value={{ lockedTypes, onToggle }}>
      <div className="lock-grid">{children}</div>
    </LockGridContext.Provider>
  );
}

// Category component
interface CategoryProps {
  category: LockCategory;
  types: LockType[];
}

function Category({ category, types }: CategoryProps) {
  const { lockedTypes, onToggle } = useLockGridContext();

  return (
    <Section header={category}>
      {types.map(type => (
        <LockToggle
          key={type}
          type={type}
          locked={lockedTypes.has(type)}
          onToggle={() => onToggle(type)}
        />
      ))}
    </Section>
  );
}

// Export compound component
export const LockGrid = Object.assign(LockGridRoot, {
  Category,
});

// Usage:
// <LockGrid lockedTypes={locked} onToggle={handleToggle}>
//   <LockGrid.Category category="CONTENT" types={contentTypes} />
//   <LockGrid.Category category="URL" types={urlTypes} />
// </LockGrid>
```

---

## Hooks Patterns

### Data Fetching Hook (SWR-like)

```tsx
// src/hooks/api/useSettings.ts
import { useState, useEffect, useCallback } from 'react';
import { api } from '@/services/api';
import type { ChatSettings } from '@/types';

interface UseSettingsResult {
  data: ChatSettings | null;
  isLoading: boolean;
  error: Error | null;
  mutate: (settings: Partial<ChatSettings>) => Promise<void>;
  refetch: () => Promise<void>;
}

export function useSettings(chatId: number): UseSettingsResult {
  const [data, setData] = useState<ChatSettings | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetchSettings = useCallback(async () => {
    try {
      setIsLoading(true);
      setError(null);
      const settings = await api.getSettings(chatId);
      setData(settings);
    } catch (err) {
      setError(err as Error);
    } finally {
      setIsLoading(false);
    }
  }, [chatId]);

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

  const mutate = useCallback(async (updates: Partial<ChatSettings>) => {
    // Optimistic update
    setData(prev => prev ? { ...prev, ...updates } : null);

    try {
      const updated = await api.updateSettings(chatId, updates);
      setData(updated);
    } catch (err) {
      // Rollback on error
      await fetchSettings();
      throw err;
    }
  }, [chatId, fetchSettings]);

  return {
    data,
    isLoading,
    error,
    mutate,
    refetch: fetchSettings,
  };
}
```

### Telegram Hooks

```tsx
// src/hooks/telegram/useTelegramAuth.ts
import { useMemo } from 'react';
import { useInitData, useInitDataRaw } from '@telegram-apps/sdk-react';

export interface TelegramUser {
  id: number;
  firstName: string;
  lastName?: string;
  username?: string;
  isPremium: boolean;
  languageCode?: string;
}

export function useTelegramAuth() {
  const initData = useInitData();
  const initDataRaw = useInitDataRaw();

  const user = useMemo<TelegramUser | null>(() => {
    if (!initData?.user) return null;
    return {
      id: initData.user.id,
      firstName: initData.user.firstName,
      lastName: initData.user.lastName,
      username: initData.user.username,
      isPremium: initData.user.isPremium ?? false,
      languageCode: initData.user.languageCode,
    };
  }, [initData]);

  const getAuthHeader = useCallback(() => {
    if (!initDataRaw) return {};
 
Files: 1
Size: 21.4 KB
Complexity: 24/100
Category: Design

Related in Design