Интеграция API и Управление Состоянием
Данный проект реализует комплексную интеграцию с REST API и систему управления глобальным состоянием для мобильного приложения кофейни "Coffee Park". Включает работу с внешними API, Context API для тематизации и Redux Toolkit для управления данными приложения.
Приложение интегрируется с MockAPI - специализированным REST API для тестирования и прототипирования:
Базовый URL: https://68c87dc55d8d9f514735822f.mockapi.io/api/v1
- Эндпоинты:
GET /products- получение списка товаровGET /products/{id}- детали товараPOST /products- создание товара
- Данные: Информация о кофе, цены, описания, изображения
- Фильтрация: По категориям, доступности, рейтингу
- Эндпоинты:
GET /cart?userId={id}- корзина пользователяPOST /cart- добавление в корзинуPUT /cart/{id}- обновление количестваDELETE /cart/{id}- удаление из корзины
- Данные: Товары в корзине, количество, пользователь
- Эндпоинты:
GET /users- список пользователейPOST /users- регистрацияPUT /users/{id}- обновление профиля
- Данные: Профили, настройки, предпочтения
- Эндпоинты:
GET /orders?userId={id}- заказы пользователяPOST /orders- создание заказаPUT /orders/{id}- обновление статуса
- Данные: История заказов, статусы, детали
- Эндпоинты:
GET /categories- список категорий
- Данные: Названия категорий, иконки, описания
Файл: services/apiService.js
const MOCK_API_BASE_URL = 'https://68c87dc55d8d9f514735822f.mockapi.io/api/v1';
// Универсальная функция для запросов
const makeRequest = async (endpoint, options = {}) => {
try {
const url = `${MOCK_API_BASE_URL}/${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
},
...options,
};
const response = await fetch(url, config);
if (!response.ok) {
if (response.status === 404) {
return createApiResponse([]);
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return createApiResponse(data);
} catch (error) {
return createErrorResponse(error.message, 'NETWORK_ERROR');
}
};export const productApi = {
// Получение списка товаров с фильтрацией
async getProducts(filters = {}) {
let endpoint = 'products';
const params = new URLSearchParams();
if (filters.categoryId) {
params.append('categoryId', filters.categoryId);
}
if (filters.available !== undefined) {
params.append('isAvailable', filters.available);
}
if (params.toString()) {
endpoint += `?${params.toString()}`;
}
return await makeRequest(endpoint);
},
// Получение товара по ID
async getProductById(productId) {
return await makeRequest(`products/${productId}`);
},
};export const cartApi = {
// Получение корзины пользователя
async getCart(userId) {
return await makeRequest(`cart?userId=${encodeURIComponent(userId)}`);
},
// Добавление товара в корзину с полными данными
async addToCart(productData, quantity = 1, userId) {
const cartItem = {
productId: productData.id,
quantity,
userId,
// Сохраняем все данные товара
...productData,
addedAt: new Date().toISOString(),
};
return await makeRequest('cart', {
method: 'POST',
body: JSON.stringify(cartItem),
});
},
// Обновление количества товара
async updateCartItem(cartItemId, quantity) {
return await makeRequest(`cart/${cartItemId}`, {
method: 'PUT',
body: JSON.stringify({ quantity }),
});
},
};orderApi- управление заказамиuserApi- профили пользователейcategoryApi- категории товаровpaymentMethodsApi- способы оплатыloyaltyApi- программа лояльности
// Стандартизированные ответы API
const createApiResponse = (data, status = 'success') => ({
status,
data,
message:
status === 'success'
? 'Operation completed successfully'
: 'Operation failed',
});
const createErrorResponse = (message, code = 'ERROR') => ({
status: 'error',
data: null,
message,
code,
});Файл: components/ProductList.jsx
import { ScrollView, ActivityIndicator } from 'react-native';
import ProductCard from './ProductCard';
const ProductList = ({ products = [], onAddToCart, loading = false }) => {
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#6B4E3D" />
<Text style={styles.loadingText}>Загрузка товаров...</Text>
</View>
);
}
return (
<ScrollView style={styles.container}>
<View style={styles.grid}>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={() => onAddToCart && onAddToCart(product)}
/>
))}
</View>
</ScrollView>
);
};Файл: screens/HomeScreen.jsx
import { useSelector, useDispatch } from 'react-redux';
import { fetchProducts } from '../store/productsSlice';
const HomeScreen = () => {
const dispatch = useDispatch();
const products = useSelector((state) => state.products.items);
const loading = useSelector((state) => state.products.loading);
const error = useSelector((state) => state.products.error);
useEffect(() => {
// Загрузка товаров при монтировании компонента
dispatch(fetchProducts({ available: true }));
}, [dispatch]);
return (
<View style={styles.container}>
<Categories onCategoryChange={handleCategoryChange} />
<ProductList
products={products}
onAddToCart={handleAddToCart}
loading={loading}
/>
</View>
);
};// Для больших списков используется FlatList
<FlatList
data={products}
renderItem={({ item }) => (
<ProductCard product={item} onAddToCart={() => handleAddToCart(item)} />
)}
keyExtractor={(item) => item.id.toString()}
numColumns={2}
showsVerticalScrollIndicator={false}
/>{
loading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={theme.brandColor} />
<Text style={styles.loadingText}>{t('loading')}</Text>
</View>
);
}// Показ заглушек во время загрузки
{
loading ? (
<View style={styles.skeletonCard}>
<View style={styles.skeletonImage} />
<View style={styles.skeletonText} />
<View style={styles.skeletonPrice} />
</View>
) : (
<ProductCard product={product} />
);
}const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const loadData = async () => {
try {
setLoading(true);
setError(null);
const response = await productApi.getProducts();
if (response.status === 'success') {
setProducts(response.data);
} else {
throw new Error(response.message);
}
} catch (err) {
setError(err.message);
// Автоматический retry для сетевых ошибок
if (retryCount < 3 && err.message.includes('Network')) {
setTimeout(() => {
setRetryCount((prev) => prev + 1);
loadData();
}, 2000);
}
} finally {
setLoading(false);
}
};// Отображение ошибок пользователю
{
error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Ошибка загрузки: {error}</Text>
<TouchableOpacity onPress={handleRetry}>
<Text style={styles.retryButton}>Повторить</Text>
</TouchableOpacity>
</View>
);
}// Categories.jsx - переход с параметром категории
const handleCategoryPress = (category) => {
navigation.navigate('Products', {
category: category.id,
categoryName: category.name,
});
};
// ProductsScreen.jsx - получение и использование параметров
const ProductsScreen = ({ route }) => {
const { category } = route.params || {};
useEffect(() => {
// Загрузка товаров для конкретной категории
dispatch(fetchProducts({ categoryId: category }));
}, [category]);
};// Переход к деталям товара
<TouchableOpacity
onPress={() =>
navigation.navigate('ProductDetails', {
productId: product.id,
product: product, // передаем полный объект
})
}
>
<ProductCard product={product} />
</TouchableOpacity>Выбранные аспекты для Context API:
- Тематизация (светлая/темная тема)
- Локализация (мультиязычность)
- Настройки пользователя
Файл: store/themeSlice.js (Redux Toolkit реализация)
import { createSlice } from '@reduxjs/toolkit';
import { lightTheme, darkTheme } from '../themes/colors';
const themeSlice = createSlice({
name: 'theme',
initialState: {
isDark: false,
theme: lightTheme,
},
reducers: {
toggleTheme: (state) => {
state.isDark = !state.isDark;
state.theme = state.isDark ? darkTheme : lightTheme;
},
setTheme: (state, action) => {
state.isDark = action.payload === 'dark';
state.theme = state.isDark ? darkTheme : lightTheme;
},
},
});
export const { toggleTheme, setTheme } = themeSlice.actions;
export default themeSlice.reducer;// Использование темы в компонентах
import { useSelector } from 'react-redux';
const ProductCard = ({ product }) => {
const theme = useSelector((state) => state.theme.theme);
return (
<View style={[styles.card, { backgroundColor: theme.cardBackground }]}>
<Text style={[styles.title, { color: theme.primaryText }]}>
{product.name}
</Text>
</View>
);
};// Settings компонент
const SettingsScreen = () => {
const dispatch = useDispatch();
const isDark = useSelector((state) => state.theme.isDark);
return (
<TouchableOpacity
onPress={() => dispatch(toggleTheme())}
style={styles.themeToggle}
>
<Text>{isDark ? 'Светлая тема' : 'Темная тема'}</Text>
</TouchableOpacity>
);
};Файл: store/store.js
import { configureStore } from '@reduxjs/toolkit';
// Импорт всех slice reducers
import cartReducer from './cartSlice';
import productsReducer from './productsSlice';
import categoriesReducer from './categoriesSlice';
import ordersReducer from './ordersSlice';
import userReducer from './userSlice';
import themeReducer from './themeSlice';
import appReducer from './appSlice';
const store = configureStore({
reducer: {
cart: cartReducer,
products: productsReducer,
categories: categoriesReducer,
orders: ordersReducer,
user: userReducer,
theme: themeReducer,
app: appReducer,
},
});
export default store;Файл: store/cartSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { cartApi } from '../services/apiService';
// Async thunks для API операций
export const fetchCart = createAsyncThunk(
'cart/fetchCart',
async (userId, { rejectWithValue }) => {
try {
const response = await cartApi.getCart(userId);
if (response.status === 'success') {
return response.data.filter((item) => item.userId === userId);
}
return [];
} catch (err) {
return rejectWithValue(err.message);
}
}
);
export const addToCart = createAsyncThunk(
'cart/addToCart',
async ({ productData, userId }, { dispatch, rejectWithValue }) => {
try {
// Проверяем существующие товары в корзине
const cartResp = await cartApi.getCart(userId);
const userCartItems =
cartResp.data?.filter((item) => item.userId === userId) || [];
const existingItem = userCartItems.find(
(item) => String(item.productId) === String(productData.id)
);
let opResp;
if (existingItem) {
// Увеличиваем количество существующего товара
opResp = await cartApi.updateCartItem(
existingItem.id,
existingItem.quantity + 1
);
} else {
// Добавляем новый товар с полными данными
opResp = await cartApi.addToCart(productData, 1, userId);
}
if (opResp?.status === 'success') {
dispatch(fetchCart(userId));
}
return opResp;
} catch (err) {
return rejectWithValue(err.message);
}
}
);
export const updateCartItemQuantity = createAsyncThunk(
'cart/updateCartItemQuantity',
async ({ id, newQuantity, userId }, { dispatch, rejectWithValue }) => {
try {
let opResp;
if (newQuantity <= 0) {
opResp = await cartApi.removeFromCart(id);
} else {
opResp = await cartApi.updateCartItem(id, newQuantity);
}
if (opResp?.status === 'success') {
dispatch(fetchCart(userId));
}
return opResp;
} catch (err) {
return rejectWithValue(err.message);
}
}
);
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
loading: false,
error: null,
},
reducers: {
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
// fetchCart cases
.addCase(fetchCart.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchCart.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchCart.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
// addToCart cases
.addCase(addToCart.pending, (state) => {
state.loading = true;
})
.addCase(addToCart.fulfilled, (state) => {
state.loading = false;
})
.addCase(addToCart.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});
export const { clearError } = cartSlice.actions;
export default cartSlice.reducer;import { useSelector, useDispatch } from 'react-redux';
import { updateCartItemQuantity, fetchCart } from '../store/cartSlice';
const CartScreen = () => {
const dispatch = useDispatch();
const cartItems = useSelector((state) => state.cart.items);
const loading = useSelector((state) => state.cart.loading);
const userId = useSelector((state) => state.user.profile?.id);
useEffect(() => {
if (userId) {
dispatch(fetchCart(userId));
}
}, [userId, dispatch]);
const handleQuantityChange = (itemId, newQuantity) => {
dispatch(
updateCartItemQuantity({
id: itemId,
newQuantity,
userId,
})
);
};
return (
<ScrollView>
{cartItems.map((item) => (
<CartItem
key={item.id}
item={item}
onUpdateQuantity={(quantity) =>
handleQuantityChange(item.id, quantity)
}
/>
))}
</ScrollView>
);
};export const fetchProducts = createAsyncThunk(
'products/fetchProducts',
async (filters = {}, { rejectWithValue }) => {
try {
const response = await productApi.getProducts(filters);
if (response.status === 'success') {
return response.data;
}
return rejectWithValue(response.message);
} catch (err) {
return rejectWithValue(err.message);
}
}
);
const productsSlice = createSlice({
name: 'products',
initialState: {
items: [],
loading: false,
error: null,
},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});services/
└── apiService.js # Все API
store/
├── store.js # Конфигурация Redux store
├── cartSlice.js # Управление корзиной
├── productsSlice.js # Каталог товаров
├── categoriesSlice.js # Категории
├── ordersSlice.js # Заказы
├── userSlice.js # Пользователи
├── themeSlice.js # Темы (Context API альтернатива)
└── appSlice.js # Локализация и общие настройки
components/
├── ProductList.jsx # Списки с API данными
├── ProductCard.jsx # Карточки товаров
├── CartItem.jsx # Элементы корзины
└── Categories.jsx # Категории с навигацией
screens/
├── HomeScreen.jsx # Интеграция products API
├── CartScreen.jsx # Управление корзиной Redux
├── ProductsScreen.jsx # Фильтрация по API
└── ProfileScreen.jsx # Настройки пользователя
// API URLs
const MOCK_API_BASE_URL = 'https://68c87dc55d8d9f514735822f.mockapi.io/api/v1';
// Error codes
const ERROR_CODES = {
NETWORK_ERROR: 'NETWORK_ERROR',
NOT_FOUND: 'NOT_FOUND',
VALIDATION_ERROR: 'VALIDATION_ERROR',
};
// Default values
const DEFAULT_RETRY_COUNT = 3;
const DEFAULT_TIMEOUT = 5000;- REST API выбор: MockAPI для кофейни с релевантными данными
- Fetch API: Используется для всех HTTP запросов
- Модульность: Отдельный файл
apiService.jsс 8 API модулями - useState/useReducer: Redux Toolkit для state management
- FlatList/ScrollView: ProductList с оптимизированным рендерингом
- renderItem: Кастомные компоненты ProductCard, CartItem
- keyExtractor: Уникальные ID для всех списков
- Интеграция компонентов: Все существующие компоненты используют API данные
- ActivityIndicator: Состояния загрузки для всех экранов
- Error handling: Централизованная обработка с retry механизмом
- Network errors: Graceful degradation и fallback состояния
- User feedback: Понятные сообщения об ошибках
- Интеграция списков: Все экраны используют API данные
- Параметры навигации: category, productId, orderId передача
- Детальные экраны: ProductDetailsScreen с данными из API
- @reduxjs/toolkit: Современный Redux с createSlice
- Store configuration: Централизованное управление состоянием
- Async thunks: API интеграция с createAsyncThunk
- useSelector/useDispatch: Во всех компонентах
- Модульность: Отдельные slice файлы для каждой области
- Темы: themeSlice для переключения светлой/темной темы
- Локализация: appSlice для мультиязычности
- Provider интеграция: Redux Provider в App.jsx
Проект: Coffee Park App
API: MockAPI REST endpoints
State Management: Redux Toolkit + Context API
Технологии: React Native, Fetch API, AsyncThunk