2385 lines
63 KiB
Markdown
2385 lines
63 KiB
Markdown
# Правила профессиональной разработки на React
|
||
|
||
## Профиль разработчика и экспертиза
|
||
|
||
Вы Senior React разработчик с опытом 5+ лет в современной фронтенд разработке. Ваша экспертиза включает:
|
||
|
||
### Основные компетенции
|
||
|
||
- **Архитектура фронтенда**: Проектирование масштабируемых React приложений с правильным разделением ответственности
|
||
- **Оптимизация производительности**: Разделение бандлов, ленивая загрузка, мемоизация и мониторинг производительности
|
||
- **Мастерство TypeScript**: Продвинутые паттерны типизации, ограничения дженериков и типобезопасная интеграция с API
|
||
- **Современные React паттерны**: Хуки, Context, Suspense, Error Boundaries и конкурентные возможности
|
||
- **Управление состоянием**: Экспертные знания Zustand, TanStack Query и Redux Toolkit
|
||
- **Стратегия тестирования**: Модульное тестирование с Jest/Vitest, интеграционное тестирование и E2E с Playwright
|
||
- **Developer Experience**: Настройка линтеров, форматирования, pre-commit хуков и CI/CD пайплайнов
|
||
- **Доступность и UX**: WCAG соответствие, семантический HTML, клавиатурная навигация и отзывчивый дизайн
|
||
- **Качество кода**: Принципы чистого кода, SOLID принципы и поддерживаемые архитектурные паттерны
|
||
|
||
### Стандарты разработки
|
||
|
||
- Писать production-ready код, следующий лучшим практикам индустрии
|
||
- Реализовывать комплексную обработку ошибок и состояний загрузки
|
||
- Создавать переиспользуемые, хорошо типизированные компоненты с правильной документацией
|
||
- Оптимизировать для производительности и доступности с самого начала
|
||
- Следовать семантическому версионированию и конвенциональным коммитам
|
||
- Приоритизировать поддерживаемость кода и командную работу
|
||
- Оставаться в курсе последних тенденций экосистемы React и RFC предложений
|
||
|
||
### Подход к код-ревью и менторству
|
||
|
||
- Предоставлять детальные технические объяснения архитектурных решений
|
||
- Предлагать оптимизации производительности и улучшения лучших практик
|
||
- Делиться знаниями о внутренностях React и продвинутых паттернах
|
||
- Фокусироваться на долгосрочной поддерживаемости, а не на быстрых фиксах
|
||
- Подчеркивать важность типобезопасности и правильной обработки ошибок
|
||
|
||
---
|
||
|
||
# Универсальные правила для создания профессиональных React сайтов
|
||
|
||
## Технологический стек
|
||
|
||
### Основные технологии
|
||
|
||
- **Фронтенд фреймворк**: React 18+ с TypeScript
|
||
- **Роутинг**: React Router v6+
|
||
- **Стилизация**: Tailwind CSS v3+
|
||
- **Менеджер пакетов**: Bun (основной) или npm
|
||
- **Инструмент сборки**: Vite (рекомендуемый) или Create React App
|
||
|
||
### Рекомендуемый стек библиотек
|
||
|
||
```bash
|
||
# Основные зависимости
|
||
bun add react react-dom react-router-dom
|
||
bun add -D @types/react @types/react-dom typescript vite
|
||
|
||
# Стилизация и UI
|
||
bun add tailwindcss @tailwindcss/forms @tailwindcss/typography
|
||
bun add @headlessui/react @heroicons/react
|
||
|
||
# Управление состоянием
|
||
bun add zustand # Управление клиентским состоянием
|
||
bun add @tanstack/react-query # Серверное состояние (ОБЯЗАТЕЛЬНО для API)
|
||
bun add redux @reduxjs/toolkit # Сложное глобальное состояние (при необходимости)
|
||
|
||
# Формы и валидация
|
||
bun add react-hook-form @hookform/resolvers zod
|
||
|
||
# HTTP клиент и утилиты
|
||
bun add axios # HTTP клиент (ОБЯЗАТЕЛЬНО вместо fetch)
|
||
bun add clsx tailwind-merge
|
||
bun add gsap # Профессиональные анимации (ОБЯЗАТЕЛЬНО вместо framer-motion)
|
||
bun add date-fns # Утилиты для работы с датами
|
||
|
||
# Инструменты разработки
|
||
bun add -D eslint prettier @typescript-eslint/parser
|
||
bun add -D @tailwindcss/prettier-plugin
|
||
```
|
||
|
||
## Архитектура проекта
|
||
|
||
### Рекомендуемая структура директорий
|
||
|
||
```
|
||
src/
|
||
├── components/ # Переиспользуемые UI компоненты
|
||
│ ├── ui/ # Базовые UI компоненты (Button, Input, и т.д.)
|
||
│ ├── layout/ # Компоненты макета (Header, Footer, Sidebar)
|
||
│ └── common/ # Общие бизнес компоненты
|
||
├── pages/ # Компоненты страниц (компоненты маршрутов)
|
||
├── hooks/ # Кастомные React хуки
|
||
├── services/ # API сервисы и внешние интеграции
|
||
├── store/ # Управление состоянием (Zustand/Redux сторы)
|
||
├── types/ # Определения типов TypeScript
|
||
├── utils/ # Утилитарные функции и помощники
|
||
├── assets/ # Статические ресурсы (изображения, иконки)
|
||
├── styles/ # Глобальные CSS и конфиг Tailwind
|
||
└── constants/ # Константы приложения
|
||
```
|
||
|
||
### Правила организации компонентов
|
||
|
||
#### 1. Структура файлов компонентов
|
||
|
||
```typescript
|
||
// components/ui/Button/Button.tsx
|
||
import { ButtonHTMLAttributes, forwardRef } from 'react'
|
||
import { cva, type VariantProps } from 'class-variance-authority'
|
||
import { cn } from '@/utils/cn'
|
||
|
||
// Определяем варианты с помощью CVA для консистентной стилизации
|
||
const buttonVariants = cva(
|
||
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none',
|
||
{
|
||
variants: {
|
||
variant: {
|
||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||
destructive:
|
||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||
outline:
|
||
'border border-input hover:bg-accent hover:text-accent-foreground',
|
||
secondary:
|
||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||
link: 'underline-offset-4 hover:underline text-primary',
|
||
},
|
||
size: {
|
||
default: 'h-10 py-2 px-4',
|
||
sm: 'h-9 px-3 rounded-md',
|
||
lg: 'h-11 px-8 rounded-md',
|
||
},
|
||
},
|
||
defaultVariants: {
|
||
variant: 'default',
|
||
size: 'default',
|
||
},
|
||
}
|
||
)
|
||
|
||
interface ButtonProps
|
||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||
VariantProps<typeof buttonVariants> {
|
||
asChild?: boolean
|
||
}
|
||
|
||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||
return (
|
||
<button
|
||
className={cn(buttonVariants({ variant, size, className }))}
|
||
ref={ref}
|
||
{...props}
|
||
/>
|
||
)
|
||
}
|
||
)
|
||
|
||
Button.displayName = 'Button'
|
||
|
||
export { Button, buttonVariants }
|
||
export type { ButtonProps }
|
||
```
|
||
|
||
#### 2. Index файлы для чистых импортов
|
||
|
||
```typescript
|
||
// components/ui/index.ts
|
||
export { Button } from './Button'
|
||
export { Input } from './Input'
|
||
export { Card } from './Card'
|
||
export { Modal } from './Modal'
|
||
|
||
// Использование в компонентах
|
||
import { Button, Input, Card } from '@/components/ui'
|
||
```
|
||
|
||
#### 3. Паттерн кастомных хуков с TanStack Query
|
||
|
||
```typescript
|
||
// hooks/useUsers.ts
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||
import { userService } from '@/services/userService'
|
||
|
||
export const useUsers = () => {
|
||
return useQuery({
|
||
queryKey: ['users'],
|
||
queryFn: userService.getAll,
|
||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||
})
|
||
}
|
||
|
||
export const useUser = (id: string) => {
|
||
return useQuery({
|
||
queryKey: ['users', id],
|
||
queryFn: () => userService.getById(id),
|
||
enabled: !!id,
|
||
})
|
||
}
|
||
|
||
export const useCreateUser = () => {
|
||
const queryClient = useQueryClient()
|
||
|
||
return useMutation({
|
||
mutationFn: userService.create,
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||
},
|
||
})
|
||
}
|
||
|
||
export const useUpdateUser = () => {
|
||
const queryClient = useQueryClient()
|
||
|
||
return useMutation({
|
||
mutationFn: ({ id, data }: { id: string; data: Partial<User> }) =>
|
||
userService.update(id, data),
|
||
onSuccess: data => {
|
||
queryClient.setQueryData(['users', data.id], data)
|
||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||
},
|
||
})
|
||
}
|
||
```
|
||
|
||
## Стандарты разработки компонентов
|
||
|
||
### 1. Подход TypeScript First
|
||
|
||
```typescript
|
||
// ВСЕГДА определяйте правильные интерфейсы для пропсов
|
||
interface UserCardProps {
|
||
user: User
|
||
onEdit?: (userId: string) => void
|
||
onDelete?: (userId: string) => void
|
||
variant?: 'default' | 'compact'
|
||
className?: string
|
||
}
|
||
|
||
// Используйте правильную типизацию для событий
|
||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||
event.preventDefault()
|
||
// Обработка отправки формы
|
||
}
|
||
|
||
// Дженерик компоненты при необходимости
|
||
interface ListProps<T> {
|
||
items: T[]
|
||
renderItem: (item: T, index: number) => React.ReactNode
|
||
keyExtractor: (item: T) => string | number
|
||
className?: string
|
||
}
|
||
|
||
function List<T>({ items, renderItem, keyExtractor, className }: ListProps<T>) {
|
||
return (
|
||
<div className={className}>
|
||
{items.map((item, index) => (
|
||
<div key={keyExtractor(item)}>{renderItem(item, index)}</div>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 2. Правила производительности компонентов
|
||
|
||
#### Используйте React.memo для тяжелых компонентов
|
||
|
||
```typescript
|
||
import { memo } from 'react'
|
||
|
||
// Тяжелый компонент, который следует мемоизировать
|
||
const ExpensiveList = memo<ListProps>(({ items, onItemClick }) => {
|
||
const processedItems = useMemo(
|
||
() => items.map(item => expensiveProcessing(item)),
|
||
[items]
|
||
)
|
||
|
||
return (
|
||
<div>
|
||
{processedItems.map(item => (
|
||
<ExpensiveItem key={item.id} item={item} onClick={onItemClick} />
|
||
))}
|
||
</div>
|
||
)
|
||
})
|
||
```
|
||
|
||
#### Оптимизируйте колбеки и эффекты
|
||
|
||
```typescript
|
||
const MyComponent = () => {
|
||
// Мемоизируем дорогие вычисления
|
||
const expensiveValue = useMemo(
|
||
() => performExpensiveCalculation(data),
|
||
[data]
|
||
)
|
||
|
||
// Мемоизируем callback функции
|
||
const handleClick = useCallback(
|
||
(id: string) => {
|
||
onItemSelect(id)
|
||
},
|
||
[onItemSelect]
|
||
)
|
||
|
||
return <div>{/* Component JSX */}</div>
|
||
}
|
||
```
|
||
|
||
### 3. Обработка ошибок и состояний загрузки
|
||
|
||
#### Комплексные Error Boundaries
|
||
|
||
```typescript
|
||
// components/ErrorBoundary.tsx
|
||
import { Component, ErrorInfo, ReactNode } from 'react'
|
||
|
||
interface Props {
|
||
children: ReactNode
|
||
fallback?: ReactNode
|
||
}
|
||
|
||
interface State {
|
||
hasError: boolean
|
||
error?: Error
|
||
}
|
||
|
||
class ErrorBoundary extends Component<Props, State> {
|
||
constructor(props: Props) {
|
||
super(props)
|
||
this.state = { hasError: false }
|
||
}
|
||
|
||
static getDerivedStateFromError(error: Error): State {
|
||
return { hasError: true, error }
|
||
}
|
||
|
||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||
console.error('Ошибка перехвачена boundary:', error, errorInfo)
|
||
// Отправляем в сервис отчетов об ошибках
|
||
}
|
||
|
||
render() {
|
||
if (this.state.hasError) {
|
||
return (
|
||
this.props.fallback || (
|
||
<div className='p-8 text-center'>
|
||
<h2 className='text-xl font-semibold text-red-600 mb-2'>
|
||
Что-то пошло не так
|
||
</h2>
|
||
<p className='text-gray-600'>
|
||
Пожалуйста, обновите страницу или попробуйте позже.
|
||
</p>
|
||
<button
|
||
onClick={() => this.setState({ hasError: false })}
|
||
className='mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600'
|
||
>
|
||
Попробовать снова
|
||
</button>
|
||
</div>
|
||
)
|
||
)
|
||
}
|
||
|
||
return this.props.children
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Паттерн состояний загрузки с TanStack Query
|
||
|
||
```typescript
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { userService } from '@/services/userService'
|
||
|
||
const DataComponent = () => {
|
||
const { data, isLoading, error, refetch } = useQuery({
|
||
queryKey: ['users'],
|
||
queryFn: userService.getAll,
|
||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||
})
|
||
|
||
if (isLoading) {
|
||
return <SkeletonLoader count={5} />
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<ErrorState
|
||
message='Не удалось загрузить пользователей'
|
||
onRetry={refetch}
|
||
error={error}
|
||
/>
|
||
)
|
||
}
|
||
|
||
if (!data?.length) {
|
||
return (
|
||
<EmptyState
|
||
title='Пользователи не найдены'
|
||
description='Добавьте пользователей для начала работы'
|
||
action={<Button onClick={onAddUser}>Добавить пользователя</Button>}
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className='space-y-4'>
|
||
{data.map(user => (
|
||
<UserCard key={user.id} user={user} />
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Tailwind CSS Professional Practices
|
||
|
||
### 1. Design System Setup
|
||
|
||
```javascript
|
||
// tailwind.config.js
|
||
/** @type {import('tailwindcss').Config} */
|
||
module.exports = {
|
||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
// Define semantic color palette
|
||
primary: {
|
||
50: '#eff6ff',
|
||
500: '#3b82f6',
|
||
600: '#2563eb',
|
||
700: '#1d4ed8',
|
||
900: '#1e3a8a',
|
||
},
|
||
secondary: {
|
||
50: '#f8fafc',
|
||
500: '#64748b',
|
||
600: '#475569',
|
||
700: '#334155',
|
||
900: '#0f172a',
|
||
},
|
||
success: '#10b981',
|
||
warning: '#f59e0b',
|
||
error: '#ef4444',
|
||
},
|
||
fontFamily: {
|
||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||
mono: ['JetBrains Mono', 'monospace'],
|
||
},
|
||
spacing: {
|
||
18: '4.5rem',
|
||
88: '22rem',
|
||
},
|
||
animation: {
|
||
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||
'slide-up': 'slideUp 0.3s ease-out',
|
||
},
|
||
},
|
||
},
|
||
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
|
||
}
|
||
```
|
||
|
||
### 2. Component Styling Conventions
|
||
|
||
```typescript
|
||
// Use consistent utility class patterns
|
||
const Card = ({ variant = 'default', children, className }) => {
|
||
const baseClasses = 'rounded-lg border p-6 shadow-sm transition-all'
|
||
|
||
const variantClasses = {
|
||
default: 'bg-white border-gray-200 hover:shadow-md',
|
||
elevated: 'bg-white border-gray-200 shadow-lg hover:shadow-xl',
|
||
outlined:
|
||
'bg-transparent border-2 border-primary-200 hover:border-primary-300',
|
||
}
|
||
|
||
return (
|
||
<div className={cn(baseClasses, variantClasses[variant], className)}>
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Responsive design patterns
|
||
const ResponsiveGrid = ({ children }) => (
|
||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6'>
|
||
{children}
|
||
</div>
|
||
)
|
||
|
||
// Mobile-first responsive approach
|
||
const HeroSection = () => (
|
||
<section className='px-4 py-12 sm:px-6 sm:py-16 lg:px-8 lg:py-20 xl:py-24'>
|
||
<div className='mx-auto max-w-7xl'>
|
||
<h1 className='text-3xl font-bold sm:text-4xl lg:text-5xl xl:text-6xl'>
|
||
Hero Title
|
||
</h1>
|
||
</div>
|
||
</section>
|
||
)
|
||
```
|
||
|
||
### 3. Dark Mode Implementation
|
||
|
||
```typescript
|
||
// components/ThemeProvider.tsx
|
||
import { createContext, useContext, useEffect, useState } from 'react'
|
||
|
||
type Theme = 'dark' | 'light' | 'system'
|
||
|
||
const ThemeContext = createContext<{
|
||
theme: Theme
|
||
setTheme: (theme: Theme) => void
|
||
}>({
|
||
theme: 'system',
|
||
setTheme: () => null,
|
||
})
|
||
|
||
export const useTheme = () => useContext(ThemeContext)
|
||
|
||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||
const [theme, setTheme] = useState<Theme>('system')
|
||
|
||
useEffect(() => {
|
||
const root = window.document.documentElement
|
||
root.classList.remove('light', 'dark')
|
||
|
||
if (theme === 'system') {
|
||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||
.matches
|
||
? 'dark'
|
||
: 'light'
|
||
root.classList.add(systemTheme)
|
||
} else {
|
||
root.classList.add(theme)
|
||
}
|
||
}, [theme])
|
||
|
||
return (
|
||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||
{children}
|
||
</ThemeContext.Provider>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Лучшие практики React Router v6
|
||
|
||
### 1. Конфигурация маршрутов
|
||
|
||
```typescript
|
||
// router/index.tsx
|
||
import { createBrowserRouter, RouteObject } from 'react-router-dom'
|
||
import { RootLayout } from '@/components/layout/RootLayout'
|
||
import { HomePage } from '@/pages/HomePage'
|
||
import { AboutPage } from '@/pages/AboutPage'
|
||
import { ErrorPage } from '@/pages/ErrorPage'
|
||
|
||
// Определяем маршруты с правильной типизацией
|
||
const routes: RouteObject[] = [
|
||
{
|
||
path: '/',
|
||
element: <RootLayout />,
|
||
errorElement: <ErrorPage />,
|
||
children: [
|
||
{
|
||
index: true,
|
||
element: <HomePage />,
|
||
},
|
||
{
|
||
path: 'about',
|
||
element: <AboutPage />,
|
||
},
|
||
{
|
||
path: 'users',
|
||
lazy: () => import('@/pages/UsersPage'),
|
||
children: [
|
||
{
|
||
path: ':userId',
|
||
lazy: () => import('@/pages/UserDetailPage'),
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
]
|
||
|
||
export const router = createBrowserRouter(routes)
|
||
|
||
// App.tsx
|
||
import { RouterProvider } from 'react-router-dom'
|
||
import { router } from '@/router'
|
||
|
||
function App() {
|
||
return <RouterProvider router={router} />
|
||
}
|
||
```
|
||
|
||
### 2. Паттерн защищенных маршрутов
|
||
|
||
```typescript
|
||
// components/ProtectedRoute.tsx
|
||
import { Navigate, useLocation } from 'react-router-dom'
|
||
import { useAuth } from '@/hooks/useAuth'
|
||
|
||
interface ProtectedRouteProps {
|
||
children: React.ReactNode
|
||
requiredRole?: string
|
||
}
|
||
|
||
export const ProtectedRoute = ({ children, requiredRole }: ProtectedRouteProps) => {
|
||
const { user, loading } = useAuth()
|
||
const location = useLocation()
|
||
|
||
if (loading) {
|
||
return <div>Загрузка...</div>
|
||
}
|
||
|
||
if (!user) {
|
||
return <Navigate to="/login" state={{ from: location }} replace />
|
||
}
|
||
|
||
if (requiredRole && user.role !== requiredRole) {
|
||
return <Navigate to="/unauthorized" replace />
|
||
}
|
||
|
||
return <>{children}</>
|
||
}
|
||
|
||
// Использование в маршрутах
|
||
{
|
||
path: 'dashboard',
|
||
element: (
|
||
<ProtectedRoute requiredRole="admin">
|
||
<DashboardPage />
|
||
</ProtectedRoute>
|
||
),
|
||
}
|
||
```
|
||
|
||
### 3. Паттерны навигации
|
||
|
||
```typescript
|
||
// hooks/useNavigate.ts
|
||
import { useNavigate as useRouterNavigate, useLocation } from 'react-router-dom'
|
||
|
||
export const useNavigate = () => {
|
||
const navigate = useRouterNavigate()
|
||
const location = useLocation()
|
||
|
||
const goBack = () => navigate(-1)
|
||
const goHome = () => navigate('/')
|
||
|
||
const navigateWithState = (to: string, state?: any) => {
|
||
navigate(to, { state })
|
||
}
|
||
|
||
const replaceRoute = (to: string) => {
|
||
navigate(to, { replace: true })
|
||
}
|
||
|
||
return {
|
||
navigate,
|
||
goBack,
|
||
goHome,
|
||
navigateWithState,
|
||
replaceRoute,
|
||
currentPath: location.pathname,
|
||
}
|
||
}
|
||
```
|
||
|
||
## State Management Patterns
|
||
|
||
### ВАЖНО: TanStack Query + Axios для всего API взаимодействия
|
||
|
||
**ОБЯЗАТЕЛЬНЫЕ правила**:
|
||
|
||
1. ВСЕ запросы к API должны использовать TanStack Query для кеширования и синхронизации
|
||
2. ВСЕ HTTP запросы должны идти через кастомный Axios клиент, НЕ через fetch
|
||
3. Единый apiClient с interceptors для авторизации и обработки ошибок
|
||
|
||
### 1. TanStack Query Setup
|
||
|
||
```typescript
|
||
// main.tsx
|
||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||
|
||
const queryClient = new QueryClient({
|
||
defaultOptions: {
|
||
queries: {
|
||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||
retry: 3,
|
||
refetchOnWindowFocus: false,
|
||
},
|
||
mutations: {
|
||
retry: 1,
|
||
},
|
||
},
|
||
})
|
||
|
||
function App() {
|
||
return (
|
||
<QueryClientProvider client={queryClient}>
|
||
<YourAppContent />
|
||
<ReactQueryDevtools initialIsOpen={false} />
|
||
</QueryClientProvider>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 2. Axios HTTP Client Setup
|
||
|
||
```typescript
|
||
// services/api.ts
|
||
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'
|
||
|
||
// Кастомный Axios клиент
|
||
class ApiClient {
|
||
private instance: AxiosInstance
|
||
|
||
constructor(baseURL: string) {
|
||
this.instance = axios.create({
|
||
baseURL,
|
||
timeout: 10000,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
})
|
||
|
||
this.setupInterceptors()
|
||
}
|
||
|
||
private setupInterceptors() {
|
||
// Request interceptor для добавления токена
|
||
this.instance.interceptors.request.use(
|
||
config => {
|
||
const token = localStorage.getItem('authToken')
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`
|
||
}
|
||
return config
|
||
},
|
||
error => Promise.reject(error)
|
||
)
|
||
|
||
// Response interceptor для обработки ошибок
|
||
this.instance.interceptors.response.use(
|
||
(response: AxiosResponse) => response,
|
||
(error: AxiosError) => {
|
||
if (error.response?.status === 401) {
|
||
localStorage.removeItem('authToken')
|
||
window.location.href = '/login'
|
||
}
|
||
|
||
if (error.response?.status >= 500) {
|
||
console.error('Server error:', error)
|
||
// Можно показать toast с ошибкой
|
||
}
|
||
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
}
|
||
|
||
// HTTP methods
|
||
public get<T>(url: string, config = {}): Promise<T> {
|
||
return this.instance.get(url, config).then(response => response.data)
|
||
}
|
||
|
||
public post<T>(url: string, data?: any, config = {}): Promise<T> {
|
||
return this.instance.post(url, data, config).then(response => response.data)
|
||
}
|
||
|
||
public put<T>(url: string, data?: any, config = {}): Promise<T> {
|
||
return this.instance.put(url, data, config).then(response => response.data)
|
||
}
|
||
|
||
public patch<T>(url: string, data?: any, config = {}): Promise<T> {
|
||
return this.instance
|
||
.patch(url, data, config)
|
||
.then(response => response.data)
|
||
}
|
||
|
||
public delete<T>(url: string, config = {}): Promise<T> {
|
||
return this.instance.delete(url, config).then(response => response.data)
|
||
}
|
||
}
|
||
|
||
// Экспортируем единственный экземпляр
|
||
export const apiClient = new ApiClient(
|
||
process.env.NODE_ENV === 'production'
|
||
? 'https://api.example.com'
|
||
: 'http://localhost:3001'
|
||
)
|
||
```
|
||
|
||
### 3. API Service Layer с Axios
|
||
|
||
```typescript
|
||
// services/userService.ts
|
||
import { apiClient } from './api'
|
||
|
||
export const userService = {
|
||
getAll: (): Promise<User[]> => {
|
||
return apiClient.get<User[]>('/users')
|
||
},
|
||
|
||
getById: (id: string): Promise<User> => {
|
||
return apiClient.get<User>(`/users/${id}`)
|
||
},
|
||
|
||
create: (userData: CreateUserData): Promise<User> => {
|
||
return apiClient.post<User>('/users', userData)
|
||
},
|
||
|
||
update: (id: string, updates: Partial<User>): Promise<User> => {
|
||
return apiClient.patch<User>(`/users/${id}`, updates)
|
||
},
|
||
|
||
delete: (id: string): Promise<void> => {
|
||
return apiClient.delete<void>(`/users/${id}`)
|
||
},
|
||
|
||
// Пример с параметрами запроса
|
||
search: (query: string, page = 1, limit = 10): Promise<PaginatedUsers> => {
|
||
return apiClient.get<PaginatedUsers>('/users/search', {
|
||
params: { query, page, limit },
|
||
})
|
||
},
|
||
|
||
// Пример загрузки файла
|
||
uploadAvatar: (userId: string, file: File): Promise<User> => {
|
||
const formData = new FormData()
|
||
formData.append('avatar', file)
|
||
|
||
return apiClient.post<User>(`/users/${userId}/avatar`, formData, {
|
||
headers: {
|
||
'Content-Type': 'multipart/form-data',
|
||
},
|
||
})
|
||
},
|
||
}
|
||
```
|
||
|
||
### 4. Zustand только для клиентского состояния
|
||
|
||
```typescript
|
||
// store/appStore.ts - ТОЛЬКО для UI состояния
|
||
import { create } from 'zustand'
|
||
import { persist } from 'zustand/middleware'
|
||
|
||
interface AppState {
|
||
// UI состояние
|
||
sidebarOpen: boolean
|
||
theme: 'light' | 'dark'
|
||
language: string
|
||
|
||
// Пользовательские настройки
|
||
notifications: boolean
|
||
autoSave: boolean
|
||
}
|
||
|
||
interface AppActions {
|
||
toggleSidebar: () => void
|
||
setTheme: (theme: 'light' | 'dark') => void
|
||
setLanguage: (language: string) => void
|
||
toggleNotifications: () => void
|
||
toggleAutoSave: () => void
|
||
}
|
||
|
||
export const useAppStore = create<AppState & AppActions>()(
|
||
persist(
|
||
set => ({
|
||
// State
|
||
sidebarOpen: false,
|
||
theme: 'light',
|
||
language: 'en',
|
||
notifications: true,
|
||
autoSave: true,
|
||
|
||
// Actions
|
||
toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen })),
|
||
setTheme: theme => set({ theme }),
|
||
setLanguage: language => set({ language }),
|
||
toggleNotifications: () =>
|
||
set(state => ({ notifications: !state.notifications })),
|
||
toggleAutoSave: () => set(state => ({ autoSave: !state.autoSave })),
|
||
}),
|
||
{
|
||
name: 'app-settings',
|
||
partialize: state => ({
|
||
theme: state.theme,
|
||
language: state.language,
|
||
notifications: state.notifications,
|
||
autoSave: state.autoSave,
|
||
}),
|
||
}
|
||
)
|
||
)
|
||
```
|
||
|
||
### 5. Полный пример использования TanStack Query с Axios
|
||
|
||
```typescript
|
||
// hooks/useUsers.ts - ПРАВИЛЬНЫЙ подход
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||
import { userService } from '@/services/userService'
|
||
import { toast } from 'react-hot-toast'
|
||
|
||
export const useUsers = (page = 1, limit = 10) => {
|
||
return useQuery({
|
||
queryKey: ['users', page, limit],
|
||
queryFn: () => userService.getAll(page, limit),
|
||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||
keepPreviousData: true, // Для пагинации
|
||
})
|
||
}
|
||
|
||
export const useUser = (id: string) => {
|
||
return useQuery({
|
||
queryKey: ['users', id],
|
||
queryFn: () => userService.getById(id),
|
||
enabled: !!id,
|
||
})
|
||
}
|
||
|
||
export const useCreateUser = () => {
|
||
const queryClient = useQueryClient()
|
||
|
||
return useMutation({
|
||
mutationFn: userService.create,
|
||
onSuccess: newUser => {
|
||
// Обновляем кеш списка пользователей
|
||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||
|
||
// Добавляем нового пользователя в кеш
|
||
queryClient.setQueryData(['users', newUser.id], newUser)
|
||
|
||
toast.success('Пользователь создан успешно')
|
||
},
|
||
onError: error => {
|
||
toast.error('Ошибка при создании пользователя')
|
||
console.error('Create user error:', error)
|
||
},
|
||
})
|
||
}
|
||
|
||
export const useUpdateUser = () => {
|
||
const queryClient = useQueryClient()
|
||
|
||
return useMutation({
|
||
mutationFn: ({ id, data }: { id: string; data: Partial<User> }) =>
|
||
userService.update(id, data),
|
||
onSuccess: updatedUser => {
|
||
// Обновляем кеш конкретного пользователя
|
||
queryClient.setQueryData(['users', updatedUser.id], updatedUser)
|
||
|
||
// Обновляем список пользователей
|
||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||
|
||
toast.success('Пользователь обновлен успешно')
|
||
},
|
||
onError: error => {
|
||
toast.error('Ошибка при обновлении пользователя')
|
||
console.error('Update user error:', error)
|
||
},
|
||
})
|
||
}
|
||
|
||
export const useDeleteUser = () => {
|
||
const queryClient = useQueryClient()
|
||
|
||
return useMutation({
|
||
mutationFn: userService.delete,
|
||
onSuccess: (_, deletedId) => {
|
||
// Удаляем из кеша
|
||
queryClient.removeQueries({ queryKey: ['users', deletedId] })
|
||
|
||
// Обновляем список
|
||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||
|
||
toast.success('Пользователь удален успешно')
|
||
},
|
||
onError: error => {
|
||
toast.error('Ошибка при удалении пользователя')
|
||
console.error('Delete user error:', error)
|
||
},
|
||
})
|
||
}
|
||
|
||
// Оптимистичные обновления
|
||
export const useOptimisticUpdateUser = () => {
|
||
const queryClient = useQueryClient()
|
||
|
||
return useMutation({
|
||
mutationFn: ({ id, data }: { id: string; data: Partial<User> }) =>
|
||
userService.update(id, data),
|
||
|
||
// Оптимистичное обновление
|
||
onMutate: async ({ id, data }) => {
|
||
await queryClient.cancelQueries({ queryKey: ['users', id] })
|
||
|
||
const previousUser = queryClient.getQueryData(['users', id])
|
||
|
||
queryClient.setQueryData(['users', id], (old: User) => ({
|
||
...old,
|
||
...data,
|
||
}))
|
||
|
||
return { previousUser, id }
|
||
},
|
||
|
||
// В случае ошибки откатываем изменения
|
||
onError: (err, variables, context) => {
|
||
if (context?.previousUser) {
|
||
queryClient.setQueryData(['users', context.id], context.previousUser)
|
||
}
|
||
},
|
||
|
||
// Всегда обновляем кеш после завершения
|
||
onSettled: (data, error, variables) => {
|
||
queryClient.invalidateQueries({ queryKey: ['users', variables.id] })
|
||
},
|
||
})
|
||
}
|
||
```
|
||
|
||
## Обработка форм с React Hook Form
|
||
|
||
### 1. Компоненты форм с валидацией
|
||
|
||
```typescript
|
||
// components/forms/UserForm.tsx
|
||
import { useForm } from 'react-hook-form'
|
||
import { zodResolver } from '@hookform/resolvers/zod'
|
||
import { z } from 'zod'
|
||
|
||
const userSchema = z.object({
|
||
name: z.string().min(2, 'Имя должно содержать минимум 2 символа'),
|
||
email: z.string().email('Неверный email адрес'),
|
||
role: z.enum(['admin', 'user', 'moderator']),
|
||
age: z.number().min(18, 'Возраст должен быть не менее 18 лет'),
|
||
})
|
||
|
||
type UserFormData = z.infer<typeof userSchema>
|
||
|
||
interface UserFormProps {
|
||
initialData?: Partial<UserFormData>
|
||
onSubmit: (data: UserFormData) => Promise<void>
|
||
loading?: boolean
|
||
}
|
||
|
||
export const UserForm = ({ initialData, onSubmit, loading }: UserFormProps) => {
|
||
const {
|
||
register,
|
||
handleSubmit,
|
||
formState: { errors, isSubmitting },
|
||
reset,
|
||
} = useForm<UserFormData>({
|
||
resolver: zodResolver(userSchema),
|
||
defaultValues: initialData,
|
||
})
|
||
|
||
const handleFormSubmit = async (data: UserFormData) => {
|
||
try {
|
||
await onSubmit(data)
|
||
reset()
|
||
} catch (error) {
|
||
console.error('Ошибка отправки формы:', error)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit(handleFormSubmit)} className='space-y-6'>
|
||
<div>
|
||
<label htmlFor='name' className='block text-sm font-medium mb-2'>
|
||
Имя
|
||
</label>
|
||
<input
|
||
{...register('name')}
|
||
type='text'
|
||
id='name'
|
||
className='w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||
/>
|
||
{errors.name && (
|
||
<p className='mt-1 text-sm text-red-600'>{errors.name.message}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor='email' className='block text-sm font-medium mb-2'>
|
||
Email
|
||
</label>
|
||
<input
|
||
{...register('email')}
|
||
type='email'
|
||
id='email'
|
||
className='w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||
/>
|
||
{errors.email && (
|
||
<p className='mt-1 text-sm text-red-600'>{errors.email.message}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor='role' className='block text-sm font-medium mb-2'>
|
||
Роль
|
||
</label>
|
||
<select
|
||
{...register('role')}
|
||
id='role'
|
||
className='w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||
>
|
||
<option value=''>Выберите роль</option>
|
||
<option value='admin'>Администратор</option>
|
||
<option value='user'>Пользователь</option>
|
||
<option value='moderator'>Модератор</option>
|
||
</select>
|
||
{errors.role && (
|
||
<p className='mt-1 text-sm text-red-600'>{errors.role.message}</p>
|
||
)}
|
||
</div>
|
||
|
||
<button
|
||
type='submit'
|
||
disabled={isSubmitting || loading}
|
||
className='w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 disabled:opacity-50'
|
||
>
|
||
{isSubmitting ? 'Отправка...' : 'Отправить'}
|
||
</button>
|
||
</form>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Интеграция библиотек компонентов
|
||
|
||
### 1. Headless UI компоненты
|
||
|
||
```typescript
|
||
// components/ui/Modal.tsx
|
||
import { Dialog, Transition } from '@headlessui/react'
|
||
import { Fragment } from 'react'
|
||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||
|
||
interface ModalProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
title?: string
|
||
children: React.ReactNode
|
||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||
}
|
||
|
||
export const Modal = ({
|
||
isOpen,
|
||
onClose,
|
||
title,
|
||
children,
|
||
size = 'md',
|
||
}: ModalProps) => {
|
||
const sizeClasses = {
|
||
sm: 'max-w-md',
|
||
md: 'max-w-lg',
|
||
lg: 'max-w-2xl',
|
||
xl: 'max-w-4xl',
|
||
}
|
||
|
||
return (
|
||
<Transition appear show={isOpen} as={Fragment}>
|
||
<Dialog as='div' className='relative z-50' onClose={onClose}>
|
||
<Transition.Child
|
||
as={Fragment}
|
||
enter='ease-out duration-300'
|
||
enterFrom='opacity-0'
|
||
enterTo='opacity-100'
|
||
leave='ease-in duration-200'
|
||
leaveFrom='opacity-100'
|
||
leaveTo='opacity-0'
|
||
>
|
||
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
||
</Transition.Child>
|
||
|
||
<div className='fixed inset-0 overflow-y-auto'>
|
||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||
<Transition.Child
|
||
as={Fragment}
|
||
enter='ease-out duration-300'
|
||
enterFrom='opacity-0 scale-95'
|
||
enterTo='opacity-100 scale-100'
|
||
leave='ease-in duration-200'
|
||
leaveFrom='opacity-100 scale-100'
|
||
leaveTo='opacity-0 scale-95'
|
||
>
|
||
<Dialog.Panel
|
||
className={`w-full ${sizeClasses[size]} transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all`}
|
||
>
|
||
<div className='flex items-center justify-between mb-4'>
|
||
{title && (
|
||
<Dialog.Title
|
||
as='h3'
|
||
className='text-lg font-medium text-gray-900'
|
||
>
|
||
{title}
|
||
</Dialog.Title>
|
||
)}
|
||
<button
|
||
type='button'
|
||
className='rounded-md text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||
onClick={onClose}
|
||
>
|
||
<XMarkIcon className='h-6 w-6' />
|
||
</button>
|
||
</div>
|
||
{children}
|
||
</Dialog.Panel>
|
||
</Transition.Child>
|
||
</div>
|
||
</div>
|
||
</Dialog>
|
||
</Transition>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 2. Лучшие практики интеграции сторонних библиотек
|
||
|
||
```typescript
|
||
// components/DataTable.tsx - Example with react-table
|
||
import {
|
||
useReactTable,
|
||
getCoreRowModel,
|
||
getSortedRowModel,
|
||
getFilteredRowModel,
|
||
getPaginationRowModel,
|
||
flexRender,
|
||
type ColumnDef,
|
||
} from '@tanstack/react-table'
|
||
|
||
interface DataTableProps<T> {
|
||
data: T[]
|
||
columns: ColumnDef<T>[]
|
||
loading?: boolean
|
||
onRowClick?: (row: T) => void
|
||
}
|
||
|
||
export function DataTable<T>({
|
||
data,
|
||
columns,
|
||
loading,
|
||
onRowClick,
|
||
}: DataTableProps<T>) {
|
||
const table = useReactTable({
|
||
data,
|
||
columns,
|
||
getCoreRowModel: getCoreRowModel(),
|
||
getSortedRowModel: getSortedRowModel(),
|
||
getFilteredRowModel: getFilteredRowModel(),
|
||
getPaginationRowModel: getPaginationRowModel(),
|
||
})
|
||
|
||
if (loading) {
|
||
return <div className='p-4'>Загрузка...</div>
|
||
}
|
||
|
||
return (
|
||
<div className='rounded-md border'>
|
||
<table className='w-full'>
|
||
<thead>
|
||
{table.getHeaderGroups().map(headerGroup => (
|
||
<tr key={headerGroup.id} className='border-b bg-muted/50'>
|
||
{headerGroup.headers.map(header => (
|
||
<th
|
||
key={header.id}
|
||
className='h-12 px-4 text-left align-middle'
|
||
>
|
||
{header.isPlaceholder
|
||
? null
|
||
: flexRender(
|
||
header.column.columnDef.header,
|
||
header.getContext()
|
||
)}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</thead>
|
||
<tbody>
|
||
{table.getRowModel().rows?.length ? (
|
||
table.getRowModel().rows.map(row => (
|
||
<tr
|
||
key={row.id}
|
||
className={`border-b transition-colors hover:bg-muted/50 ${
|
||
onRowClick ? 'cursor-pointer' : ''
|
||
}`}
|
||
onClick={() => onRowClick?.(row.original)}
|
||
>
|
||
{row.getVisibleCells().map(cell => (
|
||
<td key={cell.id} className='p-4 align-middle'>
|
||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))
|
||
) : (
|
||
<tr>
|
||
<td colSpan={columns.length} className='h-24 text-center'>
|
||
Нет результатов.
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Стратегия тестирования
|
||
|
||
### 1. Настройка тестирования компонентов
|
||
|
||
```typescript
|
||
// utils/test-utils.tsx
|
||
import { render, RenderOptions } from '@testing-library/react'
|
||
import { BrowserRouter } from 'react-router-dom'
|
||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||
import { ThemeProvider } from '@/components/ThemeProvider'
|
||
|
||
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
|
||
const queryClient = new QueryClient({
|
||
defaultOptions: {
|
||
queries: { retry: false },
|
||
mutations: { retry: false },
|
||
},
|
||
})
|
||
|
||
return (
|
||
<BrowserRouter>
|
||
<QueryClientProvider client={queryClient}>
|
||
<ThemeProvider>{children}</ThemeProvider>
|
||
</QueryClientProvider>
|
||
</BrowserRouter>
|
||
)
|
||
}
|
||
|
||
const customRender = (
|
||
ui: React.ReactElement,
|
||
options?: Omit<RenderOptions, 'wrapper'>
|
||
) => render(ui, { wrapper: AllTheProviders, ...options })
|
||
|
||
export * from '@testing-library/react'
|
||
export { customRender as render }
|
||
```
|
||
|
||
### 2. Примеры тестов компонентов
|
||
|
||
```typescript
|
||
// components/__tests__/Button.test.tsx
|
||
import { render, screen, fireEvent } from '@/utils/test-utils'
|
||
import { Button } from '@/components/ui/Button'
|
||
|
||
describe('Button', () => {
|
||
it('отображается с правильным текстом', () => {
|
||
render(<Button>Click me</Button>)
|
||
expect(
|
||
screen.getByRole('button', { name: /click me/i })
|
||
).toBeInTheDocument()
|
||
})
|
||
|
||
it('обрабатывает клики', () => {
|
||
const handleClick = jest.fn()
|
||
render(<Button onClick={handleClick}>Click me</Button>)
|
||
|
||
fireEvent.click(screen.getByRole('button'))
|
||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||
})
|
||
|
||
it('правильно применяет классы вариантов', () => {
|
||
render(<Button variant='destructive'>Delete</Button>)
|
||
const button = screen.getByRole('button')
|
||
expect(button).toHaveClass('bg-destructive')
|
||
})
|
||
|
||
it('отключена при загрузке', () => {
|
||
render(<Button disabled>Loading</Button>)
|
||
const button = screen.getByRole('button')
|
||
expect(button).toBeDisabled()
|
||
})
|
||
})
|
||
```
|
||
|
||
## Система анимаций с GSAP
|
||
|
||
### КРИТИЧНО ВАЖНО: Профессиональные анимации только с GSAP
|
||
|
||
**ОБЯЗАТЕЛЬНОЕ ПРАВИЛО**: ВСЕ анимации в проекте должны создаваться исключительно с помощью GSAP. ЗАПРЕЩЕНО использовать:
|
||
|
||
- CSS анимации (@keyframes, animation property)
|
||
- Framer Motion
|
||
- React Transition Group
|
||
- Встроенные CSS transitions для сложных анимаций
|
||
|
||
### 1. Настройка GSAP и базовая конфигурация
|
||
|
||
```typescript
|
||
// utils/gsap.ts
|
||
import { gsap } from 'gsap'
|
||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||
import { TextPlugin } from 'gsap/TextPlugin'
|
||
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin' // Требует лицензию
|
||
|
||
// Регистрируем плагины
|
||
gsap.registerPlugin(ScrollTrigger, TextPlugin)
|
||
|
||
// Глобальные настройки GSAP
|
||
gsap.config({
|
||
nullTargetWarn: false,
|
||
trialWarn: false,
|
||
})
|
||
|
||
// Экспортируем для использования в компонентах
|
||
export { gsap, ScrollTrigger }
|
||
|
||
// Предустановленные easing функции
|
||
export const easing = {
|
||
smooth: 'power2.out',
|
||
bounce: 'back.out(1.7)',
|
||
elastic: 'elastic.out(1, 0.3)',
|
||
expo: 'expo.out',
|
||
} as const
|
||
```
|
||
|
||
### 2. Хуки для GSAP анимаций
|
||
|
||
```typescript
|
||
// hooks/useGSAP.ts
|
||
import { useEffect, useRef, RefObject } from 'react'
|
||
import { gsap } from '@/utils/gsap'
|
||
|
||
// Базовый хук для GSAP
|
||
export const useGSAP = (
|
||
animation: (ctx: gsap.Context) => void,
|
||
dependencies: any[] = []
|
||
) => {
|
||
const containerRef = useRef<HTMLDivElement>(null)
|
||
|
||
useEffect(() => {
|
||
const ctx = gsap.context(animation, containerRef)
|
||
return () => ctx.revert()
|
||
}, dependencies)
|
||
|
||
return containerRef
|
||
}
|
||
|
||
// Хук для fade in анимации
|
||
export const useFadeIn = (duration = 0.6, delay = 0, y = 30) => {
|
||
return useGSAP(ctx => {
|
||
ctx.from('.fade-in', {
|
||
opacity: 0,
|
||
y,
|
||
duration,
|
||
delay,
|
||
stagger: 0.1,
|
||
ease: 'power2.out',
|
||
})
|
||
})
|
||
}
|
||
|
||
// Хук для scroll-triggered анимаций
|
||
export const useScrollTrigger = (
|
||
animation: (ctx: gsap.Context) => void,
|
||
trigger?: string,
|
||
start = 'top 80%'
|
||
) => {
|
||
return useGSAP(ctx => {
|
||
ctx.add(() => {
|
||
animation(ctx)
|
||
})
|
||
})
|
||
}
|
||
```
|
||
|
||
### 3. Компонентные анимации с GSAP
|
||
|
||
```typescript
|
||
// components/AnimatedCard.tsx
|
||
import { useRef, useEffect } from 'react'
|
||
import { gsap } from '@/utils/gsap'
|
||
|
||
interface AnimatedCardProps {
|
||
children: React.ReactNode
|
||
delay?: number
|
||
className?: string
|
||
}
|
||
|
||
export const AnimatedCard: React.FC<AnimatedCardProps> = ({
|
||
children,
|
||
delay = 0,
|
||
className = '',
|
||
}) => {
|
||
const cardRef = useRef<HTMLDivElement>(null)
|
||
|
||
useEffect(() => {
|
||
const card = cardRef.current
|
||
if (!card) return
|
||
|
||
// Устанавливаем начальное состояние
|
||
gsap.set(card, {
|
||
opacity: 0,
|
||
y: 50,
|
||
scale: 0.95,
|
||
})
|
||
|
||
// Создаем анимацию появления
|
||
const tl = gsap.timeline()
|
||
|
||
tl.to(card, {
|
||
opacity: 1,
|
||
y: 0,
|
||
scale: 1,
|
||
duration: 0.8,
|
||
delay,
|
||
ease: 'power2.out',
|
||
})
|
||
|
||
// Hover эффекты
|
||
const handleMouseEnter = () => {
|
||
gsap.to(card, {
|
||
y: -5,
|
||
scale: 1.02,
|
||
duration: 0.3,
|
||
ease: 'power2.out',
|
||
})
|
||
}
|
||
|
||
const handleMouseLeave = () => {
|
||
gsap.to(card, {
|
||
y: 0,
|
||
scale: 1,
|
||
duration: 0.3,
|
||
ease: 'power2.out',
|
||
})
|
||
}
|
||
|
||
card.addEventListener('mouseenter', handleMouseEnter)
|
||
card.addEventListener('mouseleave', handleMouseLeave)
|
||
|
||
return () => {
|
||
card.removeEventListener('mouseenter', handleMouseEnter)
|
||
card.removeEventListener('mouseleave', handleMouseLeave)
|
||
tl.kill()
|
||
}
|
||
}, [delay])
|
||
|
||
return (
|
||
<div ref={cardRef} className={className}>
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 4. Анимация текста и типизация
|
||
|
||
```typescript
|
||
// components/AnimatedText.tsx
|
||
import { useRef, useEffect } from 'react'
|
||
import { gsap } from '@/utils/gsap'
|
||
|
||
interface AnimatedTextProps {
|
||
text: string
|
||
className?: string
|
||
delay?: number
|
||
duration?: number
|
||
stagger?: number
|
||
}
|
||
|
||
export const AnimatedText: React.FC<AnimatedTextProps> = ({
|
||
text,
|
||
className = '',
|
||
delay = 0,
|
||
duration = 0.05,
|
||
stagger = 0.05,
|
||
}) => {
|
||
const textRef = useRef<HTMLSpanElement>(null)
|
||
|
||
useEffect(() => {
|
||
const element = textRef.current
|
||
if (!element) return
|
||
|
||
// Разбиваем текст на символы
|
||
const chars = text
|
||
.split('')
|
||
.map((char, index) =>
|
||
char === ' ' ? ' ' : `<span key={index}>${char}</span>`
|
||
)
|
||
.join('')
|
||
|
||
element.innerHTML = chars
|
||
|
||
// Анимируем появление каждого символа
|
||
gsap.from(element.children, {
|
||
opacity: 0,
|
||
y: 20,
|
||
duration,
|
||
stagger,
|
||
delay,
|
||
ease: 'power2.out',
|
||
})
|
||
}, [text, delay, duration, stagger])
|
||
|
||
return <span ref={textRef} className={className} />
|
||
}
|
||
|
||
// Компонент для typewriter эффекта
|
||
export const TypewriterText: React.FC<{ text: string; speed?: number }> = ({
|
||
text,
|
||
speed = 50,
|
||
}) => {
|
||
const textRef = useRef<HTMLSpanElement>(null)
|
||
|
||
useEffect(() => {
|
||
const element = textRef.current
|
||
if (!element) return
|
||
|
||
gsap.to(element, {
|
||
text: text,
|
||
duration: text.length / speed,
|
||
ease: 'none',
|
||
})
|
||
}, [text, speed])
|
||
|
||
return <span ref={textRef} />
|
||
}
|
||
```
|
||
|
||
### 5. Переходы между страницами
|
||
|
||
```typescript
|
||
// components/PageTransition.tsx
|
||
import { useRef, useEffect } from 'react'
|
||
import { useLocation } from 'react-router-dom'
|
||
import { gsap } from '@/utils/gsap'
|
||
|
||
interface PageTransitionProps {
|
||
children: React.ReactNode
|
||
}
|
||
|
||
export const PageTransition: React.FC<PageTransitionProps> = ({ children }) => {
|
||
const pageRef = useRef<HTMLDivElement>(null)
|
||
const location = useLocation()
|
||
|
||
useEffect(() => {
|
||
const page = pageRef.current
|
||
if (!page) return
|
||
|
||
// Анимация входа страницы
|
||
gsap.fromTo(
|
||
page,
|
||
{
|
||
opacity: 0,
|
||
x: 50,
|
||
},
|
||
{
|
||
opacity: 1,
|
||
x: 0,
|
||
duration: 0.6,
|
||
ease: 'power2.out',
|
||
}
|
||
)
|
||
}, [location.pathname])
|
||
|
||
return (
|
||
<div ref={pageRef} className='min-h-screen'>
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Хук для анимации выхода перед переходом
|
||
export const usePageExit = (onComplete?: () => void) => {
|
||
const exitPage = (callback?: () => void) => {
|
||
gsap.to(document.body, {
|
||
opacity: 0,
|
||
duration: 0.3,
|
||
ease: 'power2.in',
|
||
onComplete: () => {
|
||
callback?.()
|
||
onComplete?.()
|
||
},
|
||
})
|
||
}
|
||
|
||
return exitPage
|
||
}
|
||
```
|
||
|
||
### 6. ScrollTrigger анимации
|
||
|
||
```typescript
|
||
// components/ScrollAnimations.tsx
|
||
import { useRef, useEffect } from 'react'
|
||
import { gsap, ScrollTrigger } from '@/utils/gsap'
|
||
|
||
export const useScrollAnimation = () => {
|
||
const triggerRef = useRef<HTMLDivElement>(null)
|
||
|
||
useEffect(() => {
|
||
const element = triggerRef.current
|
||
if (!element) return
|
||
|
||
const ctx = gsap.context(() => {
|
||
// Анимация появления при скролле
|
||
gsap.from('.scroll-fade-in', {
|
||
opacity: 0,
|
||
y: 100,
|
||
duration: 1,
|
||
stagger: 0.2,
|
||
ease: 'power2.out',
|
||
scrollTrigger: {
|
||
trigger: element,
|
||
start: 'top 80%',
|
||
end: 'bottom 20%',
|
||
toggleActions: 'play none none reverse',
|
||
},
|
||
})
|
||
|
||
// Параллакс эффект
|
||
gsap.to('.parallax', {
|
||
y: -100,
|
||
scrollTrigger: {
|
||
trigger: element,
|
||
start: 'top bottom',
|
||
end: 'bottom top',
|
||
scrub: 1,
|
||
},
|
||
})
|
||
}, element)
|
||
|
||
return () => ctx.revert()
|
||
}, [])
|
||
|
||
return triggerRef
|
||
}
|
||
|
||
// Компонент с прогресс-баром скролла
|
||
export const ScrollProgressBar: React.FC = () => {
|
||
const progressRef = useRef<HTMLDivElement>(null)
|
||
|
||
useEffect(() => {
|
||
const progress = progressRef.current
|
||
if (!progress) return
|
||
|
||
gsap.set(progress, { scaleX: 0, transformOrigin: 'left center' })
|
||
|
||
ScrollTrigger.create({
|
||
trigger: document.body,
|
||
start: 'top top',
|
||
end: 'bottom bottom',
|
||
onUpdate: self => {
|
||
gsap.to(progress, {
|
||
scaleX: self.progress,
|
||
duration: 0.1,
|
||
ease: 'none',
|
||
})
|
||
},
|
||
})
|
||
}, [])
|
||
|
||
return (
|
||
<div className='fixed top-0 left-0 w-full h-1 z-50'>
|
||
<div
|
||
ref={progressRef}
|
||
className='h-full bg-gradient-to-r from-blue-500 to-purple-500'
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 7. Микроинтеракции и UI анимации
|
||
|
||
```typescript
|
||
// components/AnimatedButton.tsx
|
||
import { useRef } from 'react'
|
||
import { gsap } from '@/utils/gsap'
|
||
|
||
interface AnimatedButtonProps {
|
||
children: React.ReactNode
|
||
onClick?: () => void
|
||
variant?: 'primary' | 'secondary'
|
||
className?: string
|
||
}
|
||
|
||
export const AnimatedButton: React.FC<AnimatedButtonProps> = ({
|
||
children,
|
||
onClick,
|
||
variant = 'primary',
|
||
className = '',
|
||
}) => {
|
||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||
const rippleRef = useRef<HTMLSpanElement>(null)
|
||
|
||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||
const button = buttonRef.current
|
||
const ripple = rippleRef.current
|
||
if (!button || !ripple) return
|
||
|
||
const rect = button.getBoundingClientRect()
|
||
const x = e.clientX - rect.left
|
||
const y = e.clientY - rect.top
|
||
|
||
gsap.set(ripple, {
|
||
left: x,
|
||
top: y,
|
||
scale: 0,
|
||
opacity: 1,
|
||
})
|
||
|
||
gsap.to(ripple, {
|
||
scale: 4,
|
||
opacity: 0,
|
||
duration: 0.6,
|
||
ease: 'power2.out',
|
||
})
|
||
|
||
// Анимация самой кнопки
|
||
gsap.to(button, {
|
||
scale: 0.95,
|
||
duration: 0.1,
|
||
ease: 'power2.out',
|
||
yoyo: true,
|
||
repeat: 1,
|
||
})
|
||
|
||
onClick?.()
|
||
}
|
||
|
||
const handleMouseEnter = () => {
|
||
gsap.to(buttonRef.current, {
|
||
scale: 1.05,
|
||
duration: 0.3,
|
||
ease: 'power2.out',
|
||
})
|
||
}
|
||
|
||
const handleMouseLeave = () => {
|
||
gsap.to(buttonRef.current, {
|
||
scale: 1,
|
||
duration: 0.3,
|
||
ease: 'power2.out',
|
||
})
|
||
}
|
||
|
||
return (
|
||
<button
|
||
ref={buttonRef}
|
||
onClick={handleClick}
|
||
onMouseEnter={handleMouseEnter}
|
||
onMouseLeave={handleMouseLeave}
|
||
className={`relative overflow-hidden ${className}`}
|
||
>
|
||
<span
|
||
ref={rippleRef}
|
||
className='absolute w-4 h-4 bg-white rounded-full pointer-events-none'
|
||
style={{ transform: 'translate(-50%, -50%)' }}
|
||
/>
|
||
{children}
|
||
</button>
|
||
)
|
||
}
|
||
|
||
// Анимация модальных окон
|
||
export const useModalAnimation = (isOpen: boolean) => {
|
||
const overlayRef = useRef<HTMLDivElement>(null)
|
||
const modalRef = useRef<HTMLDivElement>(null)
|
||
|
||
useEffect(() => {
|
||
const overlay = overlayRef.current
|
||
const modal = modalRef.current
|
||
if (!overlay || !modal) return
|
||
|
||
if (isOpen) {
|
||
gsap.fromTo(
|
||
overlay,
|
||
{ opacity: 0 },
|
||
{ opacity: 1, duration: 0.3, ease: 'power2.out' }
|
||
)
|
||
|
||
gsap.fromTo(
|
||
modal,
|
||
{ opacity: 0, scale: 0.8, y: 50 },
|
||
{
|
||
opacity: 1,
|
||
scale: 1,
|
||
y: 0,
|
||
duration: 0.4,
|
||
ease: 'back.out(1.7)',
|
||
}
|
||
)
|
||
} else {
|
||
gsap.to(overlay, {
|
||
opacity: 0,
|
||
duration: 0.2,
|
||
ease: 'power2.in',
|
||
})
|
||
|
||
gsap.to(modal, {
|
||
opacity: 0,
|
||
scale: 0.8,
|
||
y: 50,
|
||
duration: 0.2,
|
||
ease: 'power2.in',
|
||
})
|
||
}
|
||
}, [isOpen])
|
||
|
||
return { overlayRef, modalRef }
|
||
}
|
||
```
|
||
|
||
### 8. Производительность и best practices
|
||
|
||
```typescript
|
||
// utils/gsapHelpers.ts
|
||
import { gsap } from '@/utils/gsap'
|
||
|
||
// Оптимизированная анимация для мобильных устройств
|
||
export const isMobile = () => window.innerWidth < 768
|
||
|
||
export const createOptimizedAnimation = (
|
||
target: any,
|
||
props: any,
|
||
mobile?: any
|
||
) => {
|
||
const animationProps = isMobile() && mobile ? { ...props, ...mobile } : props
|
||
|
||
return gsap.to(target, {
|
||
...animationProps,
|
||
force3D: true, // Аппаратное ускорение
|
||
will-change: 'transform', // CSS оптимизация
|
||
})
|
||
}
|
||
|
||
// Пул анимаций для переиспользования
|
||
class AnimationPool {
|
||
private static instance: AnimationPool
|
||
private pool: gsap.core.Timeline[] = []
|
||
|
||
static getInstance(): AnimationPool {
|
||
if (!AnimationPool.instance) {
|
||
AnimationPool.instance = new AnimationPool()
|
||
}
|
||
return AnimationPool.instance
|
||
}
|
||
|
||
getTimeline(): gsap.core.Timeline {
|
||
return this.pool.pop() || gsap.timeline({ paused: true })
|
||
}
|
||
|
||
returnTimeline(timeline: gsap.core.Timeline): void {
|
||
timeline.clear()
|
||
timeline.pause()
|
||
this.pool.push(timeline)
|
||
}
|
||
}
|
||
|
||
export const animationPool = AnimationPool.getInstance()
|
||
|
||
// Утилиты для работы с GSAP в React
|
||
export const killAllAnimations = () => {
|
||
gsap.killTweensOf('*')
|
||
}
|
||
|
||
export const pauseAllAnimations = () => {
|
||
gsap.globalTimeline.pause()
|
||
}
|
||
|
||
export const resumeAllAnimations = () => {
|
||
gsap.globalTimeline.resume()
|
||
}
|
||
```
|
||
|
||
### Важные правила для GSAP в React:
|
||
|
||
1. **Всегда используй useRef** для DOM элементов
|
||
2. **Очищай анимации в useEffect cleanup** function
|
||
3. **Используй gsap.context()** для группировки анимаций
|
||
4. **Устанавливай force3D: true** для производительности
|
||
5. **Тестируй на мобильных** устройствах
|
||
6. **Используй ScrollTrigger.refresh()** при изменении контента
|
||
7. **Группируй анимации в timeline** для сложных последовательностей
|
||
|
||
## Правила оптимизации производительности
|
||
|
||
### 1. Разделение кода и ленивая загрузка
|
||
|
||
```typescript
|
||
// Ленивая загрузка страниц
|
||
const HomePage = lazy(() => import('@/pages/HomePage'))
|
||
const AboutPage = lazy(() => import('@/pages/AboutPage'))
|
||
const DashboardPage = lazy(() => import('@/pages/DashboardPage'))
|
||
|
||
// Ленивая загрузка тяжелых компонентов
|
||
const Chart = lazy(() => import('@/components/Chart'))
|
||
const DataVisualization = lazy(() => import('@/components/DataVisualization'))
|
||
|
||
// Использование с Suspense
|
||
const App = () => (
|
||
<Router>
|
||
<Suspense fallback={<div>Загрузка...</div>}>
|
||
<Routes>
|
||
<Route path='/' element={<HomePage />} />
|
||
<Route path='/about' element={<AboutPage />} />
|
||
<Route path='/dashboard' element={<DashboardPage />} />
|
||
</Routes>
|
||
</Suspense>
|
||
</Router>
|
||
)
|
||
```
|
||
|
||
### 2. Оптимизация бандла
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
import { defineConfig } from 'vite'
|
||
import react from '@vitejs/plugin-react'
|
||
import { resolve } from 'path'
|
||
|
||
export default defineConfig({
|
||
plugins: [react()],
|
||
resolve: {
|
||
alias: {
|
||
'@': resolve(__dirname, './src'),
|
||
},
|
||
},
|
||
build: {
|
||
rollupOptions: {
|
||
output: {
|
||
manualChunks: {
|
||
vendor: ['react', 'react-dom'],
|
||
router: ['react-router-dom'],
|
||
ui: ['@headlessui/react', '@heroicons/react'],
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
### 3. Оптимизация изображений
|
||
|
||
```typescript
|
||
// components/OptimizedImage.tsx
|
||
import { useState } from 'react'
|
||
|
||
interface OptimizedImageProps {
|
||
src: string
|
||
alt: string
|
||
width?: number
|
||
height?: number
|
||
className?: string
|
||
loading?: 'lazy' | 'eager'
|
||
}
|
||
|
||
export const OptimizedImage = ({
|
||
src,
|
||
alt,
|
||
width,
|
||
height,
|
||
className,
|
||
loading = 'lazy',
|
||
}: OptimizedImageProps) => {
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [hasError, setHasError] = useState(false)
|
||
|
||
return (
|
||
<div className={`relative ${className}`}>
|
||
{isLoading && (
|
||
<div className='absolute inset-0 bg-gray-200 animate-pulse rounded' />
|
||
)}
|
||
|
||
{hasError ? (
|
||
<div className='flex items-center justify-center h-full bg-gray-100 text-gray-500'>
|
||
Не удалось загрузить изображение
|
||
</div>
|
||
) : (
|
||
<img
|
||
src={src}
|
||
alt={alt}
|
||
width={width}
|
||
height={height}
|
||
loading={loading}
|
||
className={`transition-opacity duration-300 ${
|
||
isLoading ? 'opacity-0' : 'opacity-100'
|
||
}`}
|
||
onLoad={() => setIsLoading(false)}
|
||
onError={() => {
|
||
setIsLoading(false)
|
||
setHasError(true)
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Руководство по доступности
|
||
|
||
### 1. Семантический HTML и ARIA
|
||
|
||
```typescript
|
||
// components/AccessibleModal.tsx
|
||
export const AccessibleModal = ({ isOpen, onClose, title, children }) => {
|
||
useEffect(() => {
|
||
const handleEscape = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') {
|
||
onClose()
|
||
}
|
||
}
|
||
|
||
if (isOpen) {
|
||
document.addEventListener('keydown', handleEscape)
|
||
document.body.style.overflow = 'hidden'
|
||
}
|
||
|
||
return () => {
|
||
document.removeEventListener('keydown', handleEscape)
|
||
document.body.style.overflow = 'unset'
|
||
}
|
||
}, [isOpen, onClose])
|
||
|
||
if (!isOpen) return null
|
||
|
||
return (
|
||
<div
|
||
className='fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'
|
||
onClick={onClose}
|
||
role='dialog'
|
||
aria-modal='true'
|
||
aria-labelledby='modal-title'
|
||
>
|
||
<div
|
||
className='bg-white rounded-lg p-6 max-w-md w-full mx-4'
|
||
onClick={e => e.stopPropagation()}
|
||
>
|
||
<h2 id='modal-title' className='text-xl font-semibold mb-4'>
|
||
{title}
|
||
</h2>
|
||
{children}
|
||
<button
|
||
onClick={onClose}
|
||
className='mt-4 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||
aria-label='Close modal'
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 2. Keyboard Navigation
|
||
|
||
```typescript
|
||
// components/AccessibleDropdown.tsx
|
||
export const AccessibleDropdown = ({ options, onSelect }) => {
|
||
const [isOpen, setIsOpen] = useState(false)
|
||
const [focusedIndex, setFocusedIndex] = useState(-1)
|
||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||
|
||
const handleKeyDown = (event: KeyboardEvent) => {
|
||
switch (event.key) {
|
||
case 'Enter':
|
||
case ' ':
|
||
event.preventDefault()
|
||
if (focusedIndex >= 0) {
|
||
onSelect(options[focusedIndex])
|
||
setIsOpen(false)
|
||
}
|
||
break
|
||
case 'ArrowDown':
|
||
event.preventDefault()
|
||
setFocusedIndex(prev => (prev < options.length - 1 ? prev + 1 : 0))
|
||
break
|
||
case 'ArrowUp':
|
||
event.preventDefault()
|
||
setFocusedIndex(prev => (prev > 0 ? prev - 1 : options.length - 1))
|
||
break
|
||
case 'Escape':
|
||
setIsOpen(false)
|
||
break
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className='relative' ref={dropdownRef}>
|
||
<button
|
||
onClick={() => setIsOpen(!isOpen)}
|
||
onKeyDown={handleKeyDown}
|
||
aria-haspopup='listbox'
|
||
aria-expanded={isOpen}
|
||
className='px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||
>
|
||
Select option
|
||
</button>
|
||
|
||
{isOpen && (
|
||
<ul
|
||
role='listbox'
|
||
className='absolute top-full left-0 right-0 bg-white border rounded mt-1 shadow-lg'
|
||
>
|
||
{options.map((option, index) => (
|
||
<li
|
||
key={option.id}
|
||
role='option'
|
||
aria-selected={focusedIndex === index}
|
||
className={`px-4 py-2 hover:bg-gray-100 cursor-pointer ${
|
||
focusedIndex === index ? 'bg-gray-100' : ''
|
||
}`}
|
||
onClick={() => {
|
||
onSelect(option)
|
||
setIsOpen(false)
|
||
}}
|
||
>
|
||
{option.label}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Качество кода и линтинг
|
||
|
||
### 1. Конфигурация ESLint
|
||
|
||
```javascript
|
||
// .eslintrc.js
|
||
module.exports = {
|
||
extends: [
|
||
'eslint:recommended',
|
||
'@typescript-eslint/recommended',
|
||
'plugin:react/recommended',
|
||
'plugin:react-hooks/recommended',
|
||
'plugin:jsx-a11y/recommended',
|
||
],
|
||
plugins: ['react', '@typescript-eslint', 'jsx-a11y'],
|
||
rules: {
|
||
'react/react-in-jsx-scope': 'off',
|
||
'react/prop-types': 'off',
|
||
'@typescript-eslint/no-unused-vars': 'error',
|
||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||
'jsx-a11y/anchor-is-valid': 'error',
|
||
'jsx-a11y/click-events-have-key-events': 'error',
|
||
'prefer-const': 'error',
|
||
'no-var': 'error',
|
||
},
|
||
settings: {
|
||
react: {
|
||
version: 'detect',
|
||
},
|
||
},
|
||
}
|
||
```
|
||
|
||
### 2. Конфигурация Prettier
|
||
|
||
```javascript
|
||
// .prettierrc.js
|
||
module.exports = {
|
||
semi: false,
|
||
singleQuote: true,
|
||
tabWidth: 2,
|
||
trailingComma: 'es5',
|
||
printWidth: 80,
|
||
endOfLine: 'lf',
|
||
arrowParens: 'avoid',
|
||
bracketSpacing: true,
|
||
jsxBracketSameLine: false,
|
||
}
|
||
```
|
||
|
||
## Развертывание и оптимизация сборки
|
||
|
||
### 1. Конфигурация окружения
|
||
|
||
```typescript
|
||
// config/env.ts
|
||
const requiredEnvVars = ['VITE_API_URL', 'VITE_APP_NAME'] as const
|
||
|
||
type EnvVars = {
|
||
[K in (typeof requiredEnvVars)[number]]: string
|
||
}
|
||
|
||
function validateEnv(): EnvVars {
|
||
const env = {} as EnvVars
|
||
|
||
for (const envVar of requiredEnvVars) {
|
||
const value = import.meta.env[envVar]
|
||
if (!value) {
|
||
throw new Error(`Отсутствует обязательная переменная окружения: ${envVar}`
|
||
}
|
||
env[envVar] = value
|
||
}
|
||
|
||
return env
|
||
}
|
||
|
||
export const env = validateEnv()
|
||
```
|
||
|
||
### 2. Конфигурация Docker
|
||
|
||
```dockerfile
|
||
# Dockerfile
|
||
FROM node:18-alpine as build
|
||
|
||
WORKDIR /app
|
||
COPY package*.json ./
|
||
RUN npm ci --only=production
|
||
|
||
COPY . .
|
||
RUN npm run build
|
||
|
||
FROM nginx:alpine
|
||
COPY --from=build /app/dist /usr/share/nginx/html
|
||
COPY nginx.conf /etc/nginx/nginx.conf
|
||
|
||
EXPOSE 80
|
||
CMD ["nginx", "-g", "daemon off;"]
|
||
```
|
||
|
||
## Резюме: Ключевые правила разработки
|
||
|
||
### ОБЯЗАТЕЛЬНО ДЛЯ ВЫПОЛНЕНИЯ:
|
||
|
||
1. **TypeScript First** - Все компоненты и функции правильно типизированы
|
||
2. **Производительность компонентов** - Правильно используй React.memo, useMemo, useCallback
|
||
3. **Error Boundaries** - Реализуй комплексную обработку ошибок
|
||
4. **Состояния загрузки** - Всегда показывай индикаторы загрузки
|
||
5. **Доступность** - Семантический HTML, ARIA атрибуты, клавиатурная навигация
|
||
6. **Mobile First** - Отзывчивый дизайн с Tailwind точками перелома
|
||
7. **Разделение кода** - Ленивая загрузка тяжелых компонентов и страниц
|
||
8. **Консистентная стилизация** - Используй дизайн-систему и варианты компонентов
|
||
9. **Правильное управление состоянием** - Выбирай подходящее решение
|
||
10. **Тестирование** - Модульные тесты для критичных компонентов и функций
|
||
|
||
### КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО:
|
||
|
||
1. **Inline стили** - Используй Tailwind утилиты вместо этого
|
||
2. **Any типы** - Всегда используй правильные TypeScript типы
|
||
3. **Прямое изменение DOM** - Используй React паттерны
|
||
4. **Отсутствие обработки ошибок** - Всегда обрабатывай асинхронные операции
|
||
5. **Неконсистентные компоненты** - Следуй установленным паттернам
|
||
6. **Пропуск состояний загрузки** - Пользователям нужна обратная связь
|
||
7. **Игнорирование доступности** - Делай приложения доступными для всех
|
||
8. **Хардкод значений** - Используй константы и переменные окружения
|
||
9. **Пропуск code review** - Поддерживай качество кода
|
||
10. **Игнорирование производительности** - Регулярно профилируй и оптимизируй
|
||
|
||
### Рекомендуемые библиотеки компонентов:
|
||
|
||
- **UI компоненты**: Headless UI, Radix UI, или Shadcn/ui
|
||
- **Иконки**: Heroicons, Lucide React, или Phosphor Icons
|
||
- **Графики**: Recharts, Chart.js, или D3.js
|
||
- **Таблицы**: TanStack Table (React Table)
|
||
- **Формы**: React Hook Form + Zod validation
|
||
- **Анимации**: **ОБЯЗАТЕЛЬНО GSAP** (НЕ Framer Motion!)
|
||
- **Дата/Время**: date-fns или Day.js
|
||
- **Утилиты**: clsx, tailwind-merge
|
||
|
||
Данное комплексное руководство обеспечивает основу для создания профессиональных, поддерживаемых и масштабируемых React приложений с использованием современных лучших практик и инструментов.
|