Привет, друзья!
На днях мне на глаза попалась статья, посвященная разработке корзины товаров на React с помощью Redux Toolkit для управления состоянием приложения и Redux Persist для хранения состояния в localStorage.
В этой заметке я покажу, как реализовать аналогичный функционал с помощью Zustand, что позволит вам наглядно убедиться в его преимуществах перед Redux как с точки зрения количества кода, так и с точки зрения производительности.
Если вам это интересно, прошу под кат.
Реализация корзины товаров с помощью Redux
С вашего позволения, я не буду полностью переводить указанную выше статью, а ограничусь кодом, непосредственно относящимся к Redux.
Итак, что нужно сделать для управления состоянием корзины товаров с помощью Redux и localStorage?
- Устанавливаем 3 зависимости:
npm i @reduxjs/toolkit react-redux redux-persist
- Определяем часть состояния (state slice) корзины:
// src/redux/cartSlice.js import { createSlice } from '@reduxjs/toolkit'; const cartSlice = createSlice({ name: 'cart', // начальное состояние initialState: { cart: [], }, reducers: { // метод для добавления товара в корзину addToCart: (state, action) => { const itemInCart = state.cart.find((item) => item.id === action.payload.id); if (itemInCart) { itemInCart.quantity++; } else { state.cart.push({ ...action.payload, quantity: 1 }); } }, // метод для увеличения количества товара, находящегося в корзине incrementQuantity: (state, action) => { const item = state.cart.find((item) => item.id === action.payload); item.quantity++; }, // метод для уменьшения количества товара, находящегося в корзине decrementQuantity: (state, action) => { const item = state.cart.find((item) => item.id === action.payload); if (item.quantity === 1) { item.quantity = 1 } else { item.quantity--; } }, // метод для удаления товара из корзины removeItem: (state, action) => { const removeItem = state.cart.filter((item) => item.id !== action.payload); state.cart = removeItem; }, }, }); // редуктор export const cartReducer = cartSlice.reducer; // операции (экшены) export const { addToCart, incrementQuantity, decrementQuantity, removeItem, } = cartSlice.actions;
- Определяем хранилище:
// src/redux/store.js import { configureStore } from "@reduxjs/toolkit"; import { cartReducer } from "./cartSlice"; export const store = configureStore({ reducer: cartReducer });
- Оборачиваем основной компонент приложения в провайдер хранилища:
// src/index.js import { Provider } from 'react-redux'; import { store } from './redux/store'; root.render( <React.StrictMode> <BrowserRouter> <Provider store={store}> <App /> </Provider> </BrowserRouter> </React.StrictMode> );
- Извлекаем состояние из хранилища с помощью хука
useSelector:
import { useSelector } from 'react-redux'; // в компоненте const cart = useSelector((state) => state.cart);
- Отправляем операции в редуктор для модификации состояния с помощью метода
dispatch, возвращаемого хукомuseDispatch:
import { useDispatch } from 'react-redux'; import { addToCart } from "../redux/cartSlice"; // в компоненте const dispatch = useDispatch(); dispatch( addToCart(item) );
- Для хранения состояния в
localStorageхранилище определяется следующим образом:
// src/redux/store.js import { configureStore } from "@reduxjs/toolkit"; import { cartReducer } from "./cartSlice"; import storage from 'redux-persist/lib/storage'; import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, } from 'redux-persist'; const persistConfig = { key: 'root', storage, }; const persistedReducer = persistReducer(persistConfig, cartReducer); export const store = configureStore({ reducer: persistedReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], }, }), }); export const persistor = persistStore(store);
- А основной компонент приложения оборачивается еще в один провайдер:
import { store, persistor } from './redux'; import { PersistGate } from 'redux-persist/integration/react'; root.render( <React.StrictMode> <BrowserRouter> <Provider store={store}> <PersistGate loading={<div>Loading...</div>} persistor={persistor}> <App /> </PersistGate> </Provider> </BrowserRouter> </React.StrictMode> );
Согласитесь, кода получилось многовато. При этом, у нас нет ни одной асинхронной операции, которые в Redux обрабатываются особым образом.
Реализации корзины товаров с помощью Zustand
Клонируем начальный проект из статьи:
git clone -b starter https://github.com/Tammibriggs/shopping-cart.git
Переходим в директорию и устанавливаем Zustand:
cd shopping-cart npm i zustand
Создаем в директории src файл store/cart.js следующего содержания:
import create from "zustand"; const useCartStore = create((set, get) => ({ // начальное состояние cart: [], // метод для добавления товара в корзину addToCart: (item) => { const { cart } = get(); const itemInCart = cart.find((i) => i.id === item.id); const newCart = itemInCart ? cart.map((i) => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i ) : [...cart, { ...item, quantity: 1 }]; set({ cart: newCart }); }, // метод для удаления товара из корзины removeItem: (id) => { const newCart = get().cart.filter((i) => i.id !== id); set({ cart: newCart }); }, // метод для увеличения количества товара, находящегося в корзине incrementQuantity: (id) => { const newCart = get().cart.map((i) => i.id === id ? { ...i, quantity: i.quantity + 1 } : i ); set({ cart: newCart }); }, // метод для уменьшения количества товара, находящегося в корзине decrementQuantity: (id) => { const newCart = get().cart.map((i) => i.id === id ? { ...i, quantity: i.quantity - 1 } : i ); set({ cart: newCart }); }, // дополнительно // метод для получения общего количества наименований товаров, находящихся в корзине getTotalItems: () => get().cart.length, // метод для получения общего количества товаров, находящихся в корзине getTotalQuantity: () => get().cart.reduce((x, y) => x + y.quantity, 0), // метод для получения общей стоимости товаров, находящихся в корзине getTotalPrice: () => get().cart.reduce((x, y) => x + y.price * y.quantity, 0), }));
Внимание: это все, что требуется для управления состоянием приложения с помощью Zustand.
Посмотрим на использование хука useCartStore в компонентах приложения.
Главная страница
На главной странице из состояния извлекается общее количество наименований товаров, находящихся в корзине:
// pages/Home.js import useCartStore from "../store/cart"; function Home() { // возвращаем объект для того, чтобы повторно рендерить компонент при любом изменении состояния const { getTotalItems } = useCartStore(({ getTotalItems }) => ({ getTotalItems, })); // ... }
Количество наименований отображается рядом с иконкой (кнопкой) корзины:
<div className="shopping-cart" onClick={() => navigate("/cart")}> <ShoppingCart id="cartIcon" /> <p>{getTotalItems() || 0}</p> </div>
Страница корзины
На странице корзины из состояние извлекаются товары, добавленные в корзину:
// pages/Cart.js const cart = useCartStore(({ cart }) => cart);
Товары используются для рендеринга соответствующих карточек:
<div> <h3>Shopping Cart</h3> {cart.map((i) => ( <CartItem key={i.id} {...i} /> ))} </div>
Карточка товара на главной странице
В карточке товара для главной странице из состояния извлекается метод для добавления товара в корзину:
// components/Item.js const addToCart = useCartStore(({ addToCart }) => addToCart);
Данный метод можно мемоизировать следующим образом:
const addItem = useCallback(() => { addToCart({ id, title, image, price }); }, []);
Вызывается он при нажатии соответствующей кнопки:
<button onClick={addItem}>Add to Cart</button>
Карточка товара на странице корзины
В карточке товара для страницы корзины из состояния извлекаются методы для удаления товара из корзины, увеличения и уменьшения количества товаров, находящихся в корзине:
// components/CartItem.js const { incrementQuantity, decrementQuantity, removeItem } = useCartStore( ({ incrementQuantity, decrementQuantity, removeItem }) => ({ incrementQuantity, decrementQuantity, removeItem, }) );
Данные методы вызываются при нажатии соответствующих кнопок:
<div className="cartItem"> <img className="cartItem__image" src={image} alt="item" /> <div className="cartItem__info"> <p className="cartItem__title">{title}</p> <p className="cartItem__price"> <small>$</small> <strong>{price}</strong> </p> <div className="cartItem__incrDec"> <button onClick={() => { if (quantity === 1) return; decrementQuantity(id); }} > - </button> <p>{quantity}</p> <button onClick={() => incrementQuantity(id)}>+</button> </div> <button className="cartItem__removeButton" onClick={() => removeItem(id)} > Remove </button> </div> </div>
Статистика
В компоненте статистики из состояния извлекаются методы для получения общего количества и стоимости товаров, находящихся в корзине:
// components/Total.js const { getTotalQuantity, getTotalPrice } = useCartStore( ({ getTotalQuantity, getTotalPrice }) => ({ getTotalQuantity, getTotalPrice, }) );
Данные методы вызываются для рендеринга соответствующих данных:
<p className="total__p"> total ({getTotalQuantity()} items) :{" "} <strong>${getTotalPrice()}</strong> </p>
Таким образом, мы видим, что управлять состоянием приложения с помощью Zustand гораздо проще, чем с помощью Redux. Для реализации корзины товаров требуется в 3 раза меньше кода.
Хранение состояния в localStorage
Для хранения состояния в localStorage с помощью Zustand достаточно обернуть функцию обратного вызова, передаваемую в create(), в посредник persist, указав ключ и используемое хранилище:
import { persist } from "zustand/middleware"; const useCartStore = create( persist( (set, get) => ({ // ... }), { name: "cart-storage", getStorage: () => localStorage, } ) );
Готово.
Сравнение производительности
Immer, встроенный в Redux Toolkit, делает его чудовищно медленным. Проведем небольшой эксперимент.
Для чистоты эксперимента удалим весь код, связанный с хранением состояния в localStorage.
Определим функции для добавления 2500 товаров в хранилище:
// redux // src/App.js // ... import { addToCart } from "./redux/cartSlice"; function App() { const dispatch = useDispatch(); const addToCart2500Items = () => { const times = []; let id = 0; for (let i = 0; i < 25; i++) { const start = performance.now(); for (let j = 0; j < 100; j++) { const item = { id: id++, title: "title", image: "image", price: "price", }; // ! dispatch(addToCart(item)); } const difference = performance.now() - start; times.push(difference); } const time = Math.round(times.reduce((a, c) => (a += c), 0) / 25); console.log("Time:", time); }; // вызываем функцию после полной загрузки страницы useEffect(() => { window.addEventListener("load", addToCart2500Items); return () => { window.removeEventListener("load", addToCart2500Items); }; }, []); // ... }
// zustand // ... import useCartStore from "./store/cart"; function App() { const addToCart = useCartStore(({ addToCart }) => addToCart); const addToCart2500Items = () => { const times = []; let id = 0; for (let i = 0; i < 25; i++) { const start = performance.now(); for (let j = 0; j < 100; j++) { const item = { id: id++, title: "title", image: "image", price: "price", }; // ! addToCart(item); } const difference = performance.now() - start; times.push(difference); } const time = Math.round(times.reduce((a, c) => (a += c), 0) / 25); console.log("Time:", time); }; useEffect(() => { window.addEventListener("load", addToCart2500Items); return () => { window.removeEventListener("load", addToCart2500Items); }; }, []); // ... }
Получаем следующие средние значения (на вашей машине эти значения, скорее всего, будут немного отличаться):
Zustand— 10 мс;Redux— 250 мс.
Внимание: для добавления в хранилище 2500 товаров Redux требуется (sic!) в 25 раз больше времени, чем Zustand.
Обновление и удаления товаров дают аналогичные результаты. Полагаю, цифры говорят сами за себя.
Что насчет размера пакетов? — спросите вы. Пожалуйста:
Это все, чем я хотел поделиться с вами в данной заметке. Надеюсь, вы нашли для себя что-то интересное и не зря потратили время. Благодарю за внимание и happy coding!
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/683726/

Добавить комментарий