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

Добавление новой фичи

Пошаговый гайд по добавлению новой функциональности.

1. Создать структуру папок

mkdir -p src/features/my-feature/{api,hooks,components}
touch src/features/my-feature/{types.ts,index.ts}

Структура:

src/features/my-feature/
├── api/
│ ├── queries.ts # GET запросы
│ └── mutations.ts # POST/PUT/DELETE
├── hooks/
│ └── use-my-feature.ts
├── components/
│ └── my-component.tsx
├── types.ts
└── index.ts

2. Определить типы

// src/features/my-feature/types.ts
export interface MyFeatureItem {
id: string;
title: string;
createdAt: Date;
}

export interface CreateMyFeatureParams {
title: string;
}

3. Создать API слой

Queries (GET)

// src/features/my-feature/api/queries.ts
import { supabase } from '@/features/auth';

export async function fetchMyFeatureItems(userId: string) {
const { data, error } = await supabase
.from('my_feature_items')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });

if (error) throw error;
return data;
}

export async function fetchMyFeatureItem(id: string) {
const { data, error } = await supabase
.from('my_feature_items')
.select('*')
.eq('id', id)
.single();

if (error) throw error;
return data;
}

Mutations (POST/PUT/DELETE)

// src/features/my-feature/api/mutations.ts
import { supabase } from '@/features/auth';
import type { CreateMyFeatureParams } from '../types';

export async function createMyFeatureItem(
userId: string,
params: CreateMyFeatureParams
) {
const { data, error } = await supabase
.from('my_feature_items')
.insert({
user_id: userId,
title: params.title,
})
.select()
.single();

if (error) throw error;
return data;
}

export async function deleteMyFeatureItem(id: string) {
const { error } = await supabase
.from('my_feature_items')
.delete()
.eq('id', id);

if (error) throw error;
}

4. Создать хуки

// src/features/my-feature/hooks/use-my-feature.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchMyFeatureItems, fetchMyFeatureItem } from '../api/queries';
import { createMyFeatureItem, deleteMyFeatureItem } from '../api/mutations';
import { useUserId } from '@/features/courses';

// Список элементов
export function useMyFeatureItems() {
const userId = useUserId();

return useQuery({
queryKey: ['my-feature', userId],
queryFn: () => fetchMyFeatureItems(userId!),
enabled: !!userId,
});
}

// Один элемент
export function useMyFeatureItem(id: string) {
return useQuery({
queryKey: ['my-feature', id],
queryFn: () => fetchMyFeatureItem(id),
enabled: !!id,
});
}

// Создание
export function useCreateMyFeature() {
const queryClient = useQueryClient();
const userId = useUserId();

return useMutation({
mutationFn: (params: CreateMyFeatureParams) =>
createMyFeatureItem(userId!, params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['my-feature'] });
},
});
}

// Удаление
export function useDeleteMyFeature() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: deleteMyFeatureItem,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['my-feature'] });
},
});
}

5. Создать компоненты

// src/features/my-feature/components/my-feature-list.tsx
import { FlatList, RefreshControl } from 'react-native';
import { useMyFeatureItems } from '../hooks/use-my-feature';
import { MyFeatureCard } from './my-feature-card';

export function MyFeatureList() {
const { data: items, isLoading, refetch, isRefetching } = useMyFeatureItems();

if (isLoading) return <LoadingSpinner />;

return (
<FlatList
data={items}
renderItem={({ item }) => <MyFeatureCard item={item} />}
keyExtractor={(item) => item.id}
refreshControl={
<RefreshControl refreshing={isRefetching} onRefresh={refetch} />
}
ListEmptyComponent={<EmptyState />}
/>
);
}
// src/features/my-feature/components/my-feature-card.tsx
import { View, Text, Pressable } from 'react-native';
import { router } from 'expo-router';
import type { MyFeatureItem } from '../types';

interface Props {
item: MyFeatureItem;
}

export function MyFeatureCard({ item }: Props) {
return (
<Pressable
className="bg-card rounded-xl p-4 mb-3"
onPress={() => router.push(`/my-feature/${item.id}`)}
>
<Text className="text-lg font-semibold text-foreground">
{item.title}
</Text>
</Pressable>
);
}

6. Настроить экспорты

// src/features/my-feature/index.ts
export * from './types';
export * from './hooks/use-my-feature';
export * from './components/my-feature-list';
export * from './components/my-feature-card';

7. Добавить роуты

// app/(app)/my-feature/index.tsx
import { MyFeatureList } from '@/features/my-feature';

export default function MyFeatureScreen() {
return (
<View className="flex-1 bg-background p-4">
<MyFeatureList />
</View>
);
}
// app/(app)/my-feature/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { useMyFeatureItem } from '@/features/my-feature';

export default function MyFeatureDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { data: item, isLoading } = useMyFeatureItem(id);

if (isLoading) return <LoadingSpinner />;

return (
<View className="flex-1 bg-background p-4">
<Text>{item?.title}</Text>
</View>
);
}

8. Добавить локализацию

// src/core/i18n/locales/en.json
{
"myFeature": {
"title": "My Feature",
"empty": "No items yet",
"create": "Create Item"
}
}
// src/core/i18n/locales/ru.json
{
"myFeature": {
"title": "Моя фича",
"empty": "Элементов пока нет",
"create": "Создать элемент"
}
}

Чеклист

  • Структура папок создана
  • Типы определены в types.ts
  • API слой (queries + mutations)
  • Хуки с TanStack Query
  • UI компоненты
  • Экспорты в index.ts
  • Роуты в app/
  • Локализация (en, ru, uk)
  • Zustand store (если нужен client state)