Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created June 14, 2025 14:10
Show Gist options
  • Save sunmeat/66fcd2582e568bff9b720a2ee7d25a66 to your computer and use it in GitHub Desktop.
Save sunmeat/66fcd2582e568bff9b720a2ee7d25a66 to your computer and use it in GitHub Desktop.
zustand example
import { useState, useEffect } from 'react';
import { create } from 'zustand'; // npm install zustand
import './App.css';
// создание zustand store для управления корзиной и товарами
const useStore = create((set, get) => ({
products: [], // список доступных товаров
cartItems: [], // товары в корзине
status: 'idle', // статус загрузки (idle, loading, succeeded, failed)
error: null, // сообщение об ошибке
// асинхронный поиск товаров через api
fetchProducts: async (searchQuery = '') => {
set({ status: 'loading', error: null }); // установка статуса загрузки и очистка ошибок
try {
const response = await fetch('https://fakestoreapi.com/products');
if (!response.ok) throw new Error('ошибка загрузки товаров');
const products = await response.json();
// фильтрация на стороне клиента для поиска
const filteredProducts = products.filter(product =>
product.title.toLowerCase().includes(searchQuery.toLowerCase())
);
set({ products: filteredProducts, status: 'succeeded' }); // сохранение товаров и статуса
} catch (error) {
set({ error: error.message, status: 'failed' }); // установка ошибки и статуса
}
},
// добавление товара в корзину
addToCart: ({ id, title, price }) => {
set(state => {
const cartItem = state.cartItems.find(item => item.id === id);
const product = state.products.find(p => p.id === id);
if (product) {
if (cartItem) {
return {
cartItems: state.cartItems.map(item =>
item.id === id ? { ...item, quantity: item.quantity + 1 } : item
)
};
} else {
return {
cartItems: [...state.cartItems, { id, title, price, quantity: 1 }]
};
}
}
return state;
});
},
// удаление товара из корзины
removeFromCart: (id) => {
set(state => ({
cartItems: state.cartItems.filter(item => item.id !== id)
}));
},
// изменение количества товара
updateQuantity: (id, quantity) => {
set(state => {
const cartItem = state.cartItems.find(item => item.id === id);
if (cartItem && quantity > 0) {
return {
cartItems: state.cartItems.map(item =>
item.id === id ? { ...item, quantity } : item
)
};
}
return state;
});
},
// очистка корзины
clearCart: () => {
set({ cartItems: [] });
},
// очистка ошибки
clearError: () => {
set({ error: null });
},
}));
// компонент поиска и списка товаров
function ProductList() {
const { products, cartItems, status, error, fetchProducts } = useStore(); // хук для получения состояния и методов
const addToCart = useStore(state => state.addToCart); // выбор метода для оптимизации рендера
const [searchQuery, setSearchQuery] = useState(''); // локальное состояние для поиска
useEffect(() => {
fetchProducts(searchQuery); // загрузка товаров при изменении запроса
}, [fetchProducts, searchQuery]);
const handleSearch = (e) => {
setSearchQuery(e.target.value); // обновление поискового запроса
};
const getAvailableStock = (productId) => {
const cartItem = cartItems.find(item => item.id === productId);
return cartItem ? 10 - cartItem.quantity : 10; // предполагаем максимум 10 единиц на товар
};
return (
<div className="product-list">
<h2 className="section-title">Поиск товаров</h2>
<input
className="search-input"
type="text"
value={searchQuery}
onChange={handleSearch}
placeholder="Введите название товара..."
/>
{status === 'loading' && <p className="loading">Загрузка...</p>}
{error && <p className="error">{error}</p>}
{status === 'succeeded' && products.length === 0 && <p className="empty">Товары не найдены</p>}
<ul className="product-grid">
{products.map(product => (
<li key={product.id} className="product-item">
<span className="product-name">{product.title}</span>
<span className="product-price">{product.price} $</span>
<span className="product-stock">Остаток: {getAvailableStock(product.id)}</span>
<button
className="cart-button"
onClick={() => addToCart({
id: product.id,
title: product.title,
price: product.price
})}
disabled={getAvailableStock(product.id) === 0}
>
Добавить в корзину
</button>
</li>
))}
</ul>
</div>
);
}
// компонент корзины
function Cart() {
const { cartItems, updateQuantity, removeFromCart, clearCart } = useStore(); // хук для получения состояния и методов
const total = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
const getMaxQuantity = () => {
return 10; // предполагаем максимум 10 единиц на товар
};
return (
<div className="cart">
<h2 className="section-title">Корзина</h2>
{cartItems.length === 0 ? (
<p className="empty">Корзина пуста</p>
) : (
<>
<ul className="cart-list">
{cartItems.map(item => (
<li key={item.id} className="cart-item">
<span className="cart-item-name">{item.title}</span>
<span className="cart-item-price">{item.price} $ x {item.quantity}</span>
<div className="cart-item-controls">
<input
type="number"
className="cart-item-quantity"
value={item.quantity}
min="1"
max={getMaxQuantity(item.id)}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value) || 1)}
/>
<button
className="cart-button remove"
onClick={() => removeFromCart(item.id)}
>
Удалить
</button>
</div>
</li>
))}
</ul>
<div className="cart-total">
<span>Итого: {total.toFixed(2)} $</span>
<button
className="cart-button clear"
onClick={clearCart}
>
Очистить корзину
</button>
</div>
</>
)}
</div>
);
}
// компонент магазина
function ShopApp() {
return (
<div className="shop-app">
<h1 className="app-title">ReactExpress</h1>
<ProductList />
<Cart />
</div>
);
}
// корневой компонент
function App() {
return <ShopApp />;
}
export default App;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment