Data Fetching
Для работы с серверными данными используется TanStack Query v5.
Конфигурация
// QueryClient настройки
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 минут
gcTime: 30 * 60 * 1000, // 30 минут (garbage collection)
retry: 2,
refetchOnWindowFocus: false,
},
},
});
Структура API слоя
Каждая фича имеет свой API слой:
src/features/courses/
├── api/
│ ├── queries.ts # GET запросы
│ └── mutations.ts # POST/PUT/DELETE
├── hooks/
│ ├── use-courses.ts # Hook для списка курсов
│ └── use-course.ts # Hook для одного курса
Queries (GET запросы)
// src/features/courses/api/queries.ts
import { supabase } from '@/features/auth';
export async function fetchCourses(userId: string) {
const { data, error } = await supabase
.from('courses')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
}
export async function fetchCourse(courseId: string) {
const { data, error } = await supabase
.from('courses')
.select(`
*,
modules (
*,
lessons (*)
)
`)
.eq('id', courseId)
.single();
if (error) throw error;
return data;
}
Query Hooks
// src/features/courses/hooks/use-courses.ts
import { useQuery } from '@tanstack/react-query';
import { fetchCourses } from '../api/queries';
import { useUserId } from './use-user-id';
export function useCourses() {
const userId = useUserId();
return useQuery({
queryKey: ['courses', userId],
queryFn: () => fetchCourses(userId!),
enabled: !!userId,
});
}
// src/features/courses/hooks/use-course.ts
import { useQuery } from '@tanstack/react-query';
import { fetchCourse } from '../api/queries';
export function useCourse(courseId: string) {
return useQuery({
queryKey: ['course', courseId],
queryFn: () => fetchCourse(courseId),
enabled: !!courseId,
});
}
Mutations (POST/PUT/DELETE)
// src/features/courses/api/mutations.ts
export async function deleteCourse(courseId: string) {
const { error } = await supabase
.from('courses')
.delete()
.eq('id', courseId);
if (error) throw error;
}
// src/features/courses/hooks/use-delete-course.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { deleteCourse } from '../api/mutations';
export function useDeleteCourse() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteCourse,
onSuccess: () => {
// Инвалидировать кэш курсов
queryClient.invalidateQueries({ queryKey: ['courses'] });
},
});
}
Optimistic Updates
// src/features/progress/hooks/use-progress-mutation.ts
export function useProgressMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateProgress,
// Optimistic update
onMutate: async (newProgress) => {
await queryClient.cancelQueries({
queryKey: ['progress', newProgress.lessonId]
});
const previous = queryClient.getQueryData(
['progress', newProgress.lessonId]
);
queryClient.setQueryData(
['progress', newProgress.lessonId],
newProgress
);
return { previous };
},
// Откат при ошибке
onError: (err, newProgress, context) => {
queryClient.setQueryData(
['progress', newProgress.lessonId],
context?.previous
);
},
});
}
Query Keys Convention
// Простые ключи
['courses'] // Все курсы
['course', courseId] // Один курс
['lessons', courseId] // Уроки курса
// С фильтрами
['courses', { userId, status }]
['progress', lessonId]
['chat', lessonId, 'history']
useUserId Hook
Конвертирует Supabase Auth UID в Prisma User ID:
// src/features/courses/hooks/use-user-id.ts
export function useUserId() {
const { session } = useSession();
const { data: userId } = useQuery({
queryKey: ['userId', session?.user.id],
queryFn: () => fetchUserIdByAuthId(session!.user.id),
enabled: !!session?.user.id,
});
return userId;
}
SSE Streaming
Для AI чата и создания курсов используется SSE:
// src/features/chat/hooks/use-streaming-chat.ts
import { fetch as expoFetch } from 'expo/fetch';
export function useStreamingChat() {
const streamMessage = async (prompt: string) => {
const response = await expoFetch(`${API_URL}/chat/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
// Обработка SSE событий
}
};
}
expo/fetch
Используем expo/fetch вместо стандартного fetch для корректной работы с ReadableStream в React Native.