Этот пример кода добавляет товары в корзину, используя react-redux с компонентами более высокого порядка (HOC). Давайте начнем с объяснения рабочего процесса redux.

Рабочий процесс для редукции выглядит следующим образом:

Диаграмма 1: рабочий процесс Redux

В нашем Root-Reducer выполняется какое-то действие, которое распространяет изменения в Store, что приводит к изменениям DOM. Прежде чем действие попадет в Root-Reducer, оно может попасть в промежуточное ПО. Промежуточное ПО — это всего лишь некоторый код, который получает действие перед Root-Reducer. Мы используем Logger Middleware для тестирования нашего кода по мере разработки. Чтобы добавить в проект это промежуточное ПО, а также Redux, добавьте в командную строку следующий код:

yarn add redux redux-logger react-redux

Мы получаем компонент под названием Provider от «react-redux», который мы будем использовать для всего проекта. Для этого добавим его в файл index.js:

index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './redux/store';
import './index.css';
import App from './App';
ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <PersistGate persistor={persistor}>
        <App />
      </PersistGate>
    </BrowserRouter>
  </Provider>,
  document.getElementById('root')
);

Как видите, ‹Provider› обернут вокруг всего проекта, который имеет доступ к объекту store, который мы получаем от redux. Провайдер действует как родитель для всего, что находится внутри компонента. Теперь, когда у нас есть провайдер, мы должны написать наш магазин и корневой редуктор.

Корневой редьюсер — это фактический базовый объект редуктора, который представляет все состояние нашего приложения.

root-reducer.js:

import { combineReducers } from 'redux';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import userReducer from './user/user.reducer';
import cartReducer from './cart/cart.reducer';
import directoryReducer from './directory/directory.reducer';
import shopReducer from './shop/shop.reducer';
const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['cart']
};
const rootReducer = combineReducers({
  user: userReducer,
  cart: cartReducer,
  directory: directoryReducer,
  shop: shopReducer
});
export default persistReducer(persistConfig, rootReducer);

Этот Root-Reducer станет фактическим кодом, который объединяет все наши другие состояния вместе. Мы делаем это, чтобы разбить код на отдельные разделы, чтобы он не был просто одним гигантским файлом.

Затем нам нужно сделать редуктор, для которого мы собираемся предпринять какие-либо действия. В этом примере мы будем работать над добавлением нескольких товаров в корзину. Далее мы должны разделить файловую структуру на конкретное действие, с которым вы имеете дело. В нашем случае мы будем иметь дело с тележкой, поэтому напишем файл cart.reducer.js.

cart.reducer.js:

import CartActionTypes from './cart.types';
import { addItemToCart, removeItemFromCart } from './cart.utils';
const INITIAL_STATE = {
  hidden: true,
  cartItems: []
};
const cartReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case CartActionTypes.TOGGLE_CART_HIDDEN:
      return {
        ...state,
        hidden: !state.hidden
      };
    case CartActionTypes.ADD_ITEM:
      return {
        ...state,
        cartItems: addItemToCart(state.cartItems, action.payload)
      };
    case CartActionTypes.REMOVE_ITEM:
      return {
        ...state,
        cartItems: removeItemFromCart(state.cartItems, action.payload)
      };
    case CartActionTypes.CLEAR_ITEM_FROM_CART:
      return {
        ...state,
        cartItems: state.cartItems.filter(
          cartItem => cartItem.id !== action.payload.id
        )
      };
    default:
      return state;
  }
};
export default cartReducer;

Редьюсеры — это просто функция, которая получает два свойства. Он получает объект состояния, который представляет последнее состояние или начальное состояние, которое является просто объектом того, что мы пытаемся сохранить. Затем он получает действие, которое является просто объектом, за которым следует тип действия. Он также поставляется с полезной нагрузкой, которую мы могли бы использовать для обновления состояния.

Обратите внимание, что корневой редьюсер импортирует редьюсер корзины, а также другие редюсеры, написанные для проекта. Все эти редукторы объединяются с помощью CombineReducers, который импортируется из Redux. Поскольку мы сделали наш корневой редуктор, а также редьюсер, нам нужно создать наш store.js.

store.js:

import { createStore, applyMiddleware } from 'redux';
import { persistStore } from 'redux-persist';
import logger from 'redux-logger';
import rootReducer from './root-reducer';
const middlewares = [];
if (process.env.NODE_ENV === 'development') {
  middlewares.push(logger);
}
export const store = createStore(rootReducer, applyMiddleware(...middlewares));
export const persistor = persistStore(store);
export default { store, persistStore };

Оглядываясь назад на диаграмму-1, мы видим, что нам нужно добавить промежуточное ПО в Store, чтобы действия отправлялись, мы могли их перехватывать и отображать. Промежуточное ПО, которое находится между запуском действий и корневым редуктором, — это просто функции, которые получают действия, что-то с ними делают, а затем передают их корневому редьюсеру. Как только хранилище создано, мы добавляем его в Provider, который виден внутри кода в index.js.

Как только все это будет сделано, нам нужно создать файлы создателя действий (которые, как мы видели, используются в cart.reducer.js).

cart.actions.js:

import CartActionTypes from './cart.types';
export const toggleCartHidden = () => ({
  type: CartActionTypes.TOGGLE_CART_HIDDEN
});
export const addItem = item => ({
  type: CartActionTypes.ADD_ITEM,
  payload: item
});
export const removeItem = item => ({
  type: CartActionTypes.REMOVE_ITEM,
  payload: item
});
export const clearItemFromCart = item => ({
  type: CartActionTypes.CLEAR_ITEM_FROM_CART,
  payload: item
});

Это просто функции, которые возвращают объекты. Каждый объект имеет правильный формат, в котором ожидается действие.

В нашем редьюсере и файлах действий мы видим, что используем одни и те же типы жестко закодированных строк. В качестве передовой практики мы хотим сделать его единообразным справочником, чтобы в случае обновления или орфографической ошибки вам нужно было перейти только в одно место, чтобы убедиться, что типы верны. Итак, мы создаем файл cart.types.js. Это будет импортировано внутри файла действия и файлов редуктора, когда это необходимо.

cart.types.js:

const CartActionTypes = {
     TOGGLE_CART_HIDDEN: "TOGGLE_CART_HIDDEN",
     ADD_ITEM: "ADD_ITEM",
     REMOVE_ITEM: "REMOVE_ITEM",
     CLEAR_ITEM_FROM_CART: "CLEAR_ITEM_FROM_CART",
     CLEAR_CART: 'CLEAR_CART'
 }
export default CartActionTypes

Далее нам нужно ввести соединение компонента более высокого порядка. Компоненты более высокого порядка — это просто функции, которые принимают два компонента в качестве аргументов, а затем возвращают вам новый компонент.

коллекция.component.jsx:

import React from 'react';
import { connect } from 'react-redux';
import CollectionItem from '../../components/collection-item/collection-item.component';
import { selectCollection } from '../../redux/shop/shop.selectors';
import {
  CollectionPageContainer,
  CollectionTitle,
  CollectionItemsContainer
} from './collection.styles';
const CollectionPage = ({ collection }) => {
  const { title, items } = collection;
  return (
    <CollectionPageContainer>
      <CollectionTitle>{title}</CollectionTitle>
      <CollectionItemsContainer>
        {items.map(item => (
          <CollectionItem key={item.id} item={item} />
        ))}
      </CollectionItemsContainer>
    </CollectionPageContainer>
  );
};
const mapStateToProps = (state, ownProps) => ({
  collection: selectCollection(ownProps.match.params.collectionId)(state)
});
export default connect(mapStateToProps)(CollectionPage);

Для подключения первая входная функция, которая позволяет нам получить доступ к состоянию, причем состояние является нашим Root-Reducer.Стандартно называется mapStateToProps, где имя свойства будет таким же, как свойство, которое мы проходят, и значение является значением. Паттерн, который вы видите при подключении, mapStateToProps будет использоваться везде, где нам нужны свойства из редукторов.

нам также нужно посмотреть на второй аргумент для подключения, который называется mapDispatchToProps. mapDispatchToProps обновляет значения в корневом редьюсере, который затем обновляет другие редьюсеры при использовании этих обновленных данных.

коллекция.items.jsx:

import React from 'react'
import {connect} from 'react-redux'
    
    
import CustomButton from '../custom-button/custom-button.component'
import {addItem} from '../../redux/cart/cart.actions.js'
import './collection-item.styles.scss'
const CollectionItem = ({ item, addItem }) => {
    const { name, price, imageUrl} = item
    return(
    <div className='collection-item'>
        <div
            className='image'
            style={{
                backgroundImage: `url(${imageUrl})`
            }}
        
        />
            
        <div className='collection-footer'>
            <span className='name'> {name}</span>
            <span className='price'> {price}</span>
            
        </div>
            <CustomButton onClick={() => addItem(item) } inverted > Add to cart </CustomButton>
    </div>
)}
const mapDispatchToProps = dispatch => ({
    addItem: item => dispatch(addItem(item))
})
export default connect(null, mapDispatchToProps)(CollectionItem)

Чтобы обновить значения в редьюсерах, мы будем использовать mapDispatchToProps, который является вторым аргументом подключения. Это получит свойство отправки и вернет объект, в котором имя реквизита любого имени, которое мы хотим использовать внутри, передается в файл cart.action.js, который импортируется в файл (в нашем случае это addItem.

Теперь нам нужно создать функции, которые будут вызываться в коде редуктора. Мы создаем файл cart.utils.js, который содержит функции, импортированные внутри файла cart.reducer.js.

cart.utils.js:

export const addItemToCart = (cartItems, cartItemToAdd) => {
    const existingCartItem = cartItems.find(cartItem => cartItem.id === cartItemToAdd.id)
if (existingCartItem) {
        return cartItems.map(cartItem =>
            cartItem.id === cartItemToAdd.id ?
            {
                ...cartItem,
                quantity: cartItem.quantity + 1
            } :
            cartItem
        );
    }
return [...cartItems, {...cartItemToAdd, quantity: 1}]
}
export const removeItemFromCart = (cartItems, cartItemToRemove) => {
    const existingCartItem = cartItems.find(cartItem => cartItem.id === cartItemToRemove.id)
if (existingCartItem.quantity === 1) { 
        return cartItems.filter(cartItem => cartItem.id !== cartItemToRemove.id)
    }
return cartItems.map(cartItem =>
        cartItem.id === cartItemToRemove.id
        ? {...cartItem, quantity: cartItem.quantity - 1}
        : cartItem
        )
}

В этом файле мы создаем любые служебные функции, которые нам нужны. Это позволяет нам содержать наши файлы в чистоте и организовывать функции, которые мы могли бы использовать в разных местах на протяжении всей разработки нашего кода.

Теперь мы можем написать селектор. Селектор — это код, который получает весь объект состояния, но затем при необходимости извлекает небольшую часть соответствующего состояния. Мы создаем файл cart.selectors.js.

cart.selectors.js:

import { createSelector } from 'reselect'
const selectCart = state => state.cart
export const selectCartItems = createSelector(
    [selectCart], (cart) => cart.cartItems
)
export const selectCartHidden = createSelector(
    [selectCart],
    cart => cart.hidden
)
export const selectCartItemsCount = createSelector(
    [selectCartItems], cartItems => cartItems.reduce(
        (accumulatedQuantity, cartItem) =>
            accumulatedQuantity + cartItem.quantity, 0)
)
export const selectCartTotal = createSelector(
    [selectCartItems],
    cartItems => cartItems.reduce(
        (accumulatedQuantity, cartItem) =>
        accumulatedQuantity + cartItem.price * cartItem.quantity, 0)

Функция reduce() перерисовывается каждый раз, когда изменяется состояние, поэтому написание кода внутри компонента замедляет производительность проекта из-за необходимости обращаться к mapStateToProps для обновления данных. Поэтому мы создаем файл cart.selector.js со всеми необходимыми функциями, и он нацелен на определенные состояния, чтобы при взаимодействии пользователей с веб-сайтом требовалось меньше повторного рендеринга.

Затем мы можем импортировать необходимые данные в файл checkout.component.jsx.

проверка.component.jsx:

import React from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import { selectCartItems, selectCartTotal} from '../../redux/cart/cart.selectors'
import CheckoutITem from '../../components/checkout-item/checkout-item.component'
import StripeCheckoutButton from '../../components/stripe-button/stripe-button.component'
import './checkout.style.scss'
const CheckoutPage = ({cartItems, total}) => (
    <div className='checkout-page'>
        <div className='checkout-header'>
            <div className='heaser-block'>
                <span> Product</span>
            </div>
            <div className='heaser-block'>
                <span>Description</span>
            </div>
            <div className='heaser-block'>
                <span>Quantity</span>
            </div>
            <div className='heaser-block'>
                <span>Price</span>
            </div>
            <div className='heaser-block'>
                <span>Remove</span>
            </div>
        </div>
        <div>
            {cartItems.map(cartItem => (
                <CheckoutITem key={cartItem.id}cartItem={cartItem}/>
            ))}
        </div>
        <div className='total'><span> ${total} </span> </div>
        <div className='test-warning'>
            *Please use the following test credit card for payments*
             <br />
            4242 4242 4242 4242 - Exp:01/22 - CVV: 123
             
        </div>
        <StripeCheckoutButton price={total} />
    </div>
)
const mapStateToProps = createStructuredSelector({
    cartItems: selectCartItems,
    total: selectCartTotal
})
export default connect(mapStateToProps)(CheckoutPage)

В этом коде вы можете видеть, что mapStateToProps получает элементы cartItems и суммирует свойства из импортированного cart.selectors. Затем это можно использовать для отображения этих данных на странице оформления заказа.

Итак, мы приступаем к реализации redux с использованием HOC в корзину, которая обновляется, когда в нее добавляются элементы в проекте. Но вы должны задать себе вопрос, когда я использую это и зачем мне нужно мучиться с настройкой всех этих файлов и методов?

Redux позволяет создавать крупномасштабные приложения, где вам нужна поддерживающая его экосистема, которая более эффективна. Поддержка и разработка Redux дает вам гораздо больше гибкости, включая обработку асинхронных событий и возможность повторного использования компонентов гораздо лучшим способом.

Если вы создаете небольшой или средний проект, такой как проект портфолио или целевая страница (не так много повторно используемого кода или требуется много управления состоянием), лучше использовать что-то вроде контекстного API. Вам не нужно будет вводить структуры файлов и папок для избыточности для управления состоянием. Так что решайте, что лучше всего подходит для развития вашего проекта.

Если вам интересно увидеть полный проект для этого сайта, нажмите здесь! Или, если вы хотите получить полный код этого проекта, используя Redux с HOC, загляните в мой репозиторий Github! Если у вас есть другие вопросы, пожалуйста, свяжитесь со мной здесь, на моем Linkedin!