Перейти к основному содержимому

Соглашения по коду

Структура проекта

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-casecourse-card.tsx
Хукиuse-*.tsuse-courses.ts
Типыtypes.tstypes.ts
Утилитыkebab-caseformat-duration.ts
Stores*-store.tschat-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