Добавление новой фичи
Пошаговый гайд по добавлению новой функциональности.
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)