Зачем в State Manager класть карточки товара?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Зачем хранить карточки товара в State Manager
Хранение карточек товара в State Manager (Redux, Zustand, MobX и т.д.) - это принципиальное архитектурное решение, которое имеет множество серьёзных причин. Это не просто удобство, а лучшая практика для масштабируемых приложений.
Основные причины
1. Single Source of Truth (Единый источник истины)
Если карточки хранить только локально в компонентах, мы получим дублирование данных:
// ПЛОХО - данные раскиданы по компонентам
function ProductCard({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
fetch(`/api/products/${productId}`)
.then(r => r.json())
.then(setProduct);
}, [productId]);
return <div>{product?.name}</div>;
}
// Если этот компонент отобразится 10 раз - 10 запросов на сервер!
// Или 10 разных состояний одного товара
// ХОРОШО - централизованное хранилище
function useProduct(productId) {
return useSelector(state =>
state.products.items[productId]
);
}
// Любой компонент использует одни и те же данные
2. Дедупликация данных
Один товар может быть отображён в разных местах приложения одновременно:
// Сценарий:
// 1. Пользователь открыл карточку товара (ProductPage)
// 2. В углу экрана товар отображается в корзине (CartSidebar)
// 3. Товар рекомендуется в списке похожих (RelatedProducts)
// 4. Товар показан в истории просмотров (BrowsingHistory)
// БЕЗ State Manager - товар хранится в 4 разных местах
// Если цена изменилась - нужно обновить 4 копии
// С State Manager - один источник истины
const store = {
products: {
items: {
'123': { id: '123', name: 'Товар', price: 100, ... }
}
}
};
// Все компоненты получают одну копию
function ProductCard({ productId }) {
const product = useSelector(state => state.products.items[productId]);
return <div>{product.name} - {product.price}</div>;
}
// Если обновить product['123'], все компоненты обновятся
3. Синхронизация между компонентами
Когда товар изменяется в одном месте, остальные компоненты должны знать об этом:
// Сценарий: пользователь добавил товар в корзину
// Нужно обновить:
// - Иконку корзины (повысить счётчик)
// - Баннер "Товар в корзине"
// - Кнопку "Добавить в корзину" -> "В корзине"
// - Страницу корзины (если открыта)
// С State Manager
const addToCart = (productId) => {
dispatch({
type: 'ADD_TO_CART',
payload: { productId }
});
// Все подписанные компоненты обновятся автоматически
};
// Без State Manager - нужно пробрасывать callbacks через props
// или использовать EventEmitter - намного сложнее
4. Кэширование и оптимизация запросов
State Manager позволяет кэшировать данные и избежать дублирующихся запросов:
// С State Manager + Redux Thunk/Saga
const fetchProduct = (productId) => async (dispatch, getState) => {
// Проверяем уже ли загружен товар
const product = getState().products.items[productId];
if (product) {
// Уже есть в кэше, не делаем запрос
return;
}
dispatch({ type: 'FETCH_PRODUCT_START' });
try {
const response = await fetch(`/api/products/${productId}`);
const data = await response.json();
dispatch({
type: 'FETCH_PRODUCT_SUCCESS',
payload: data
});
} catch (error) {
dispatch({ type: 'FETCH_PRODUCT_ERROR', payload: error });
}
};
// Без State Manager - сложно отслеживать кэш
5. Нормализация данных
Это критично для сложных связей между сущностями:
// БЕЗ нормализации (плохо - много дублирования)
const denormalizedData = [
{
id: 1,
name: 'Товар 1',
category: { id: 'cat1', name: 'Категория 1' },
reviews: [
{ id: 'rev1', author: { id: 'user1', name: 'John' }, ... },
{ id: 'rev2', author: { id: 'user2', name: 'Jane' }, ... }
]
}
];
// Если обновится категория или автор - нужно искать по всем товарам
// С нормализацией (хорошо - одна копия каждой сущности)
const normalizedData = {
products: {
'1': { id: 1, name: 'Товар 1', categoryId: 'cat1' }
},
categories: {
'cat1': { id: 'cat1', name: 'Категория 1' }
},
reviews: {
'rev1': { id: 'rev1', productId: '1', authorId: 'user1' },
'rev2': { id: 'rev2', productId: '1', authorId: 'user2' }
},
users: {
'user1': { id: 'user1', name: 'John' },
'user2': { id: 'user2', name: 'Jane' }
}
};
// Обновиться категория или автор - одно место
6. Отслеживание состояния загрузки
State Manager позволяет управлять состояниями:
const state = {
products: {
items: { /* товары */ },
loading: false,
error: null,
loaded: false
}
};
// С таким состоянием легко показать лоадер или ошибку
function ProductPage({ productId }) {
const product = useSelector(state => state.products.items[productId]);
const loading = useSelector(state => state.products.loading);
const error = useSelector(state => state.products.error);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return <ProductCard product={product} />;
}
7. Time-Travel Debugging
С Redux DevTools можно просмотреть всю историю изменений состояния:
// Redux DevTools позволяет:
// - Просмотреть каждое изменение состояния
// - Откатиться на шаг назад
// - Посмотреть какой экшн привёл к изменению
// - Воспроизвести ошибку
// Это невозможно без State Manager
8. Middleware и побочные эффекты
State Manager позволяет обработать побочные эффекты однообразно:
// Пример Redux Thunk
const fetchProductWithRelated = (productId) => async (dispatch) => {
// Загружаем товар
dispatch(fetchProduct(productId));
// Загружаем похожие товары
dispatch(fetchRelatedProducts(productId));
// Загружаем отзывы
dispatch(fetchReviews(productId));
// Логируем
console.log(`Loaded product ${productId}`);
};
// Или с Saga/RTK Query - более сложные сценарии
9. Undo/Redo функциональность
Легко реализуется с State Manager:
const undoReducer = (state, action) => {
switch(action.type) {
case 'UNDO':
return state.history[state.history.length - 2];
case 'REDO':
return state.history[state.history.length];
default:
return {
...state,
history: [...state.history, state]
};
}
};
10. Тестирование
Чистое хранилище легче тестировать:
// Редюсер - чистая функция, легко тестировать
describe('productReducer', () => {
it('should add product to cart', () => {
const initialState = { items: [] };
const action = {
type: 'ADD_TO_CART',
payload: { productId: '123' }
};
const newState = productReducer(initialState, action);
expect(newState.items).toContain('123');
});
});
Практический пример: интернет-магазин
// State структура
const storeState = {
products: {
byId: {
'1': { id: '1', name: 'Товар 1', price: 100 },
'2': { id: '2', name: 'Товар 2', price: 200 }
},
allIds: ['1', '2'],
loading: false,
error: null
},
cart: {
items: ['1', '2'],
total: 300
},
filters: {
category: 'electronics',
priceRange: [0, 1000],
sort: 'price-asc'
}
};
// Компоненты просто подписываются
function ProductCard({ productId }) {
const product = useSelector(state => state.products.byId[productId]);
const inCart = useSelector(state => state.cart.items.includes(productId));
const dispatch = useDispatch();
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button
onClick={() => dispatch({ type: 'ADD_TO_CART', payload: productId })}
>
{inCart ? 'В корзине' : 'Добавить'}
</button>
</div>
);
}
Когда НЕ использовать State Manager
- Очень маленькие приложения (простой сайт-визитка)
- Данные, которые используются только в одном компоненте
- Временные UI состояния (открыт ли modal, скрыт ли sidebar)
- Данные, которые частто меняются локально
Для этого лучше использовать обычный useState, Context API или локальное состояние компонента.
Вывод
Хранение карточек товара в State Manager - это архитектурное решение, которое обеспечивает:
- Единственный источник истины
- Синхронизацию между компонентами
- Кэширование и оптимизацию
- Нормализацию данных
- Отслеживание состояния
- Лучшую отладку и тестирование
Это критично для любого приложения среднего и большого размера.