Заметка о Redux и Zustand

от автора

Привет, друзья!

На днях мне на глаза попалась статья, посвященная разработке корзины товаров на 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);     };   }, []);    // ... }

Получаем следующие средние значения (на вашей машине эти значения, скорее всего, будут немного отличаться):

  • Zustand10 мс;
  • Redux250 мс.

Внимание: для добавления в хранилище 2500 товаров Redux требуется (sic!) в 25 раз больше времени, чем Zustand.

Обновление и удаления товаров дают аналогичные результаты. Полагаю, цифры говорят сами за себя.

Что насчет размера пакетов? — спросите вы. Пожалуйста:

Это все, чем я хотел поделиться с вами в данной заметке. Надеюсь, вы нашли для себя что-то интересное и не зря потратили время. Благодарю за внимание и happy coding!



ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/683726/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *