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

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.