Соглашения по коду
Структура проекта
Feature-based architecture
src/features/{feature}/
├── api/ # Supabase запросы
├── hooks/ # React hooks
├── components/ # UI компоненты фичи
├── types.ts # TypeScript типы
└── index.ts # Публичные экспорты
Правила:
- Каждая фича изолирована
- Импорты только через
index.ts - Не импортировать internal файлы напрямую
// ✅ Хорошо
import { useCourses, CourseCard } from '@/features/courses';
// ❌ Плохо
import { useCourses } from '@/features/courses/hooks/use-courses';
Именование
Файлы
| Тип | Формат | Пример |
|---|---|---|
| Компоненты | kebab-case | course-card.tsx |
| Хуки | use-*.ts | use-courses.ts |
| Типы | types.ts | types.ts |
| Утилиты | kebab-case | format-duration.ts |
| Stores | *-store.ts | chat-store.ts |
Компоненты
// PascalCase для компонентов
export function CourseCard() { }
// Props interface с суффиксом Props
interface CourseCardProps {
course: Course;
onPress: () => void;
}
Хуки
// Начинаются с use
export function useCourses() { }
export function useDeleteCourse() { }
export function useProgressMutation() { }
Query Keys
// Массив строк, от общего к частному
['courses'] // Все курсы
['courses', userId] // Курсы пользователя
['course', courseId] // Один курс
['progress', lessonId] // Прогресс урока
TypeScript
Типы vs Интерфейсы
// Используй interface для объектов
interface Course {
id: string;
title: string;
}
// Используй type для unions/intersections
type Status = 'loading' | 'error' | 'success';
type CourseWithProgress = Course & { progress: number };
Строгая типизация
// ✅ Хорошо — явные типы
function useCourse(courseId: string): UseQueryResult<Course> { }
// ❌ Плохо — any
function useCourse(courseId: any) { }
Типы для props
// Используй PropsWithChildren для children
import { PropsWithChildren } from 'react';
interface CardProps {
title: string;
}
function Card({ title, children }: PropsWithChildren<CardProps>) { }
Компоненты
Структура компонента
// 1. Imports
import { View, Text } from 'react-native';
import { useTranslation } from 'react-i18next';
// 2. Types
interface Props {
title: string;
}
// 3. Component
export function MyComponent({ title }: Props) {
// 3.1 Hooks
const { t } = useTranslation();
const [state, setState] = useState();
// 3.2 Derived values
const formattedTitle = title.toUpperCase();
// 3.3 Handlers
const handlePress = () => { };
// 3.4 Render
return (
<View>
<Text>{formattedTitle}</Text>
</View>
);
}
Стилизация
// Используй NativeWind классы
<View className="bg-card rounded-xl p-4 shadow-md">
<Text className="text-lg font-semibold text-foreground">
Title
</Text>
</View>
// Не используй StyleSheet для простых стилей
// StyleSheet только для сложных/динамических стилей
State Management
Когда Zustand
- UI состояние (модалки, активные табы)
- Ephemeral state (состояние игр)
- Настройки (тема, язык)
// Persist для настроек
persist(store, {
name: 'settings-storage',
storage: createJSONStorage(() => AsyncStorage),
})
Когда TanStack Query
- Серверные данные (курсы, уроки, прогресс)
- Данные с кэшированием
- Данные с инвалидацией
// Query для GET
const { data } = useQuery({ queryKey: ['courses'], queryFn: fetchCourses });
// Mutation для POST/PUT/DELETE
const { mutate } = useMutation({
mutationFn: createCourse,
onSuccess: () => queryClient.invalidateQueries(['courses']),
});
API Layer
Queries
// api/queries.ts
export async function fetchCourses(userId: string) {
const { data, error } = await supabase
.from('courses')
.select('*')
.eq('user_id', userId);
if (error) throw error;
return data;
}
Mutations
// api/mutations.ts
export async function deleteCourse(courseId: string) {
const { error } = await supabase
.from('courses')
.delete()
.eq('id', courseId);
if (error) throw error;
}
Навигация
Expo Router
// Программная навигация
import { router } from 'expo-router';
router.push('/course/123');
router.replace('/login');
router.back();
// Параметры
const { id } = useLocalSearchParams<{ id: string }>();
Типизированные роуты
// Используй типы для параметров
router.push(`/course/${courseId}` as `/course/${string}`);
Локализация
// Используй t() для всех строк
const { t } = useTranslation();
<Text>{t('courses.title')}</Text>
// Интерполяция
<Text>{t('courses.count', { count: 5 })}</Text>
Ошибки
Error Boundaries
// Обрабатывай ошибки на уровне экрана
function CourseScreen() {
const { data, error, isLoading } = useCourse(id);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return <CourseContent course={data} />;
}
Sentry
// Отправляй критичные ошибки
try {
await riskyOperation();
} catch (error) {
Sentry.captureException(error);
throw error;
}
Git
Commits
feat: add course creation wizard
fix: resolve SSE streaming issue
refactor: extract useProgressTracking hook
docs: update API documentation
Branches
feature/course-creation
fix/auth-session-refresh
refactor/progress-tracking