Build a Shopping Cart with React + Redux

min read

Hôm nay tôi chia sẻ với các bạn một ví dụ khá hay, đó là xây dựng shop bán sản phẩm bằng React kết hợp với sự hổ trợ quản lý trạng thái của Redux.
Về vấn đề Redux thì mình cũng có làm qua và chia sẻ ở bài viết trước rồi! bạn có xem lại bài này : Create a Simple React with Redux

 

Video Demo:


+ Cài đặt Project React

npx create-react-app create-react-redux
npm install axios bootstrap@4.2.1 redux react-redux redux-thunk react-router-dom

Đầu tiên ta sẽ lấy danh sách sản phẩm từ API sau: https://5adc8779b80f490014fb883a.mockapi.io/products . Để lấy request API mình có dùng sử hổ trợ của thư viện Axios mà mình đã cài đặt ở đầu bài!
Bạn có thể xem lại bài

Ok, giờ ta tạo đường dẫn thư mục: src/api ->tạo index.js trong đường dẫn thư mục đó!
+ src/components/index.js: 

import axios from 'axios';
//mock API
let API_URL = 'https://5adc8779b80f490014fb883a.mockapi.io';
   export default function callApi(endpoint, method = 'GET', body) {
       return axios({
           method,
           url: `${API_URL}/${endpoint}`,
           data: body
       }).catch(err => {
           console.log(err);
       });
}

Trong bài chia sẻ này mình dùng Reat + Redux, để quản lý trạng thái của giỏ hàng(Cart), Trong Redux ta cần quan tâm tới (Action,Reducers,Store) mình có chia sẻ ở bài viết trước,bạn có thể xem lại bài : Create a Simple React with Redux  để hiểu hơn về Redux
Tạo thư mục đường dẫn src/components/action, tạo index.js trong thư mục đó
+ src/components/actions/index.js 

import callApi from '../api'
export const INCREASE_QUANTITY = 'INCREASE_QUANTITY';
export const DECREASE_QUANTITY = 'DECREASE_QUANTITY';
export const GET_ALL_PRODUCT = 'GET_ALL_PRODUCT';
export const GET_NUMBER_CART = 'GET_NUMBER_CART';
export const ADD_CART = 'ADD_CART' ;
export const UPDATE_CART = 'UPDATE_CART';
export const DELETE_CART = 'DELETE_CART';

export const actFetchProductsRequest = () => {
    return (dispatch) => {
        return callApi('/products', 'GET', null).then(res => {
          
            dispatch(GetAllProduct(res.data));
        });
    }
}

/*GET_ALL_PRODUCT*/
export function GetAllProduct(payload){
    return{
        type:'GET_ALL_PRODUCT',
        payload
    }
}

/*GET NUMBER CART*/
export function GetNumberCart(){
    return{
        type:'GET_NUMBER_CART'
    }
}

export function AddCart(payload){
    return {
        type:'ADD_CART',
        payload
    }
}
export function UpdateCart(payload){
    return {
        type:'UPDATE_CART',
        payload
    }
}
export function DeleteCart(payload){
    return{
        type:'DELETE_CART',
        payload
    }
}

export function IncreaseQuantity(payload){
    return{
        type:'INCREASE_QUANTITY',
        payload
    }
}
export function DecreaseQuantity(payload){
    return{
        type:'DECREASE_QUANTITY',
        payload
    }
}

Tạo thư mục sau để cấu hình reducers, src/components/reducers, sau đó tạo index.js trong thư mục đó!
+ src/components/reducers/index.js

import { combineReducers } from 'redux';
import {GET_ALL_PRODUCT,GET_NUMBER_CART,ADD_CART, DECREASE_QUANTITY, INCREASE_QUANTITY, DELETE_CART} from  '../actions';

const initProduct = {
    numberCart:0,
    Carts:[],
    _products:[]
}

function todoProduct(state = initProduct,action){
    switch(action.type){
        case GET_ALL_PRODUCT: 
            return{
                ...state,
                _products:action.payload
            }
        case GET_NUMBER_CART:
                return{
                    ...state
                }
        case ADD_CART:
            if(state.numberCart==0){
                let cart = {
                    id:action.payload.id,
                    quantity:1,
                    name:action.payload.name,
                    image:action.payload.image,
                    price:action.payload.price
                } 
                state.Carts.push(cart); 
            }
            else{
                let check = false;
                state.Carts.map((item,key)=>{
                    if(item.id==action.payload.id){
                        state.Carts[key].quantity++;
                        check=true;
                    }
                });
                if(!check){
                    let _cart = {
                        id:action.payload.id,
                        quantity:1,
                        name:action.payload.name,
                        image:action.payload.image,
                        price:action.payload.price
                    }
                    state.Carts.push(_cart);
                }
            }
            return{
                ...state,
                numberCart:state.numberCart+1
            }
            case INCREASE_QUANTITY:
                state.numberCart++
                state.Carts[action.payload].quantity++;
              
               return{
                   ...state
               }
            case DECREASE_QUANTITY:
                let quantity = state.Carts[action.payload].quantity;
                if(quantity>1){
                    state.numberCart--;
                    state.Carts[action.payload].quantity--;
                }
              
                return{
                    ...state
                }
            case DELETE_CART:
                let quantity_ = state.Carts[action.payload].quantity;
                return{
                    ...state,
                    numberCart:state.numberCart - quantity_,
                    Carts:state.Carts.filter(item=>{
                        return item.id!=state.Carts[action.payload].id
                    })
                   
                }
        default:
            return state;
    }
}
const ShopApp = combineReducers({
    _todoProduct:todoProduct
});
export default ShopApp;

Với đoạn code dài cố đế trên, bạn có thể dễ hình dung hơn là thế này:
+ Khởi tạo dữ liệu State ban đầu như sau:

const initProduct = {
    numberCart:0,
    Carts:[],
    _products:[]
}

* numberCart: dùng lưu số lượng sản phẩm đã mua có trong giỏ hàng(Carts)
* Carts:[] : tạo mảng giỏ hàng đầu tiền là rỗng
* _products:[] dùng chứa tất cả sản phầm lấy được từ API 

+ Kiểm tra action.type==GET_ALL_PRODUCT thí ta thực thi câu lệnh dưới, để trả về danh sách sản phẩm 

 case GET_ALL_PRODUCT: 
    return{
         ...state,
         _products:action.payload
    }

+ Kiểm tra action.type==ADD_CART: 
* Nếu numberCart==0: đồng nghĩa là chưa có sản phẩm nào trong mảng Carts=>thêm sản phẩm vào mảng Carts
* Nếu numberCart>0 : 
  - mảng Carts có sản phẩm=>kiểm tra sản phẩm có mua trùng không, nếu trùng tăng quantity++
  - Nếu sản phẩm mua khác với sản phẩm trong Carts=> thêm sản phẩm đó vào Carts
  - Cuối cùng là tăng numberCart++

case ADD_CART:
            if(state.numberCart==0){
                let cart = {
                    id:action.payload.id,
                    quantity:1,
                    name:action.payload.name,
                    image:action.payload.image,
                    price:action.payload.price
                } 
                state.Carts.push(cart); 
            }
            else{
                let check = false;
                state.Carts.map((item,key)=>{
                    if(item.id==action.payload.id){
                        state.Carts[key].quantity++;
                        check=true;
                    }
                });
                if(!check){
                    let _cart = {
                        id:action.payload.id,
                        quantity:1,
                        name:action.payload.name,
                        image:action.payload.image,
                        price:action.payload.price
                    }
                    state.Carts.push(_cart);
                }
            }
            return{
                ...state,
                numberCart:state.numberCart+1
            }

+ Kiểm tra action.type==INCREASE_QUANTITY: tăng quantity++ của sản phẩm được chọn

case INCREASE_QUANTITY:
    state.numberCart++
    state.Carts[action.payload].quantity++;       
    return{
        ...state
    }

+ Kiểm tra action.type==DECREASE_QUANTITY: giảm quantity-- của sản phẩm được chọn

case DECREASE_QUANTITY:
    let quantity = state.Carts[action.payload].quantity;
    if(quantity>1){
        state.numberCart--;
            state.Carts[action.payload].quantity--;
    }          
    return{
        ...state
    }

+ Kiểm tra action.type==DELETE_CART: xóa sản phẩm được chọn

case DELETE_CART:
    let quantity_ = state.Carts[action.payload].quantity;
    return{
        ...state,
        numberCart:state.numberCart - quantity_,
        Carts:state.Carts.filter(item=>{
            return item.id!=state.Carts[action.payload].id
        })
                   
}

Tiếp ta ta cần tạo Store, hãy tạo đường dẫn sau:
+ src/components/stores/index.js

import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import ShopApp from '../reducers/index'
const store =  createStore(ShopApp,applyMiddleware(thunkMiddleware));
export default store;

+ Tạo component Header : src/components/Header.js

import React, { Component } from 'react'
import {Link} from 'react-router-dom'
import  {connect} from  'react-redux'
export class Header extends Component {
    render() {
        return (
            <div className="row">
              <div className="col-md-12">
                  <nav className="navbar  navbar-dark bg-dark ">
                        <ul className="nav">
                            <li className="nav-item" ><Link to="/" className="nav-link active">Products</Link></li>
                            <li className="nav-item"><Link to="/carts" className="nav-link">Carts : {this.props.numberCart}</Link></li>
                        </ul>
                  </nav>
              </div>
            </div>
        )
    }
}
const mapStateToProps = state =>{
    return{
        numberCart:state._todoProduct.numberCart
    }
}
export default connect(mapStateToProps,null)(Header)

Đoạn code Header.js để lấy giá trị numberCart bạn dùng mapStateToProps được hổ trợ trong redux, bạn có thể tìm hiểu thêm về thằng này 

const mapStateToProps = state =>{
    return{
        numberCart:state._todoProduct.numberCart
    }
}

Hiển thị dữ liệu ra Html: {this.props.numberCart}

Tạo file component Product hiển thị danh sách sản phẩm, gọi hàm (AddCart , actFetchProductsRequest) từ src/actions/index.js 
+ src/components/Product.js 

import React, { Component } from 'react'
import {actFetchProductsRequest,AddCart} from '../actions'
import {connect} from 'react-redux';
export class Product extends Component {
    constructor(props) {
        super(props)
       
    }

    componentDidMount(){
        this.props.actFetchProductsRequest();
    }
    
    render() {
        const {_products} = this.props._products;
        if(_products.length>0){
           
           return (
                <div className="row" style={{marginTop:'10px'}}>
                    <div className="col-md-12">
                        <div className="row">
                            {
                                _products.map((item,index)=>(
                                    <div key={index} className="col-md-2" style={{marginBottom:'10px'}}>
                                        <img src={item.image} className="img-resposive" style={{width:'100%',height:'100px'}}/>
                                        <h5>{item.name}</h5>
                                        <span className="badge badge-primary" style={{cursor:'pointer'}} onClick={()=>this.props.AddCart(item)}>Add Cart</span>
                                    </div>
                                ))
                            }
                        </div>
                    </div>
                </div>
            ) 
        }
        return(
            <div className="row">
                <h2>Loading...!</h2>
            </div>
        )
        
    }
}

const mapStateToProps = state =>{
    return {
         _products: state._todoProduct,
       };
}
function mapDispatchToProps(dispatch){
    return{
        actFetchProductsRequest:()=>dispatch(actFetchProductsRequest()),
        AddCart:item=>dispatch(AddCart(item))
     
    }
}
export default connect(mapStateToProps,mapDispatchToProps)(Product)

Đoạn code bên trên bạn cần quan tâm là hàm (actFetchProductsRequest, AddCart)
- actFetchProductsRequest(): để gọi hàm này bạn cần cài đặt nó trong mapDispatchToProps. Sau đó dùng this.props.actFetchProductsRequest() để thực thi nó
- Sau khi gọi actFetchProductsRequest() ta sẽ có được dữ liệu sản phẩm trong State, để lấy danh sách sản phẩm ra bạn cần sử dụng mapStateToProps

const mapStateToProps = state =>{
    return {
         _products: state._todoProduct,
       };
}

- Hàm AddCart bắt sự kiện thêm sản phẩm vào mảng Carts, this.props.AddCart(item)

+ Tạo component Cart: src/components/Cart.js dùng hiển thị sản phẩm đã mua có trong giỏ hàng

import React, { Component } from 'react'
import { connect } from "react-redux";
import {IncreaseQuantity,DecreaseQuantity,DeleteCart} from '../actions';

function Cart({items,IncreaseQuantity,DecreaseQuantity,DeleteCart}){
  //  console.log(items)
    let ListCart = [];
    let TotalCart=0;
    Object.keys(items.Carts).forEach(function(item){
        TotalCart+=items.Carts[item].quantity * items.Carts[item].price;
        ListCart.push(items.Carts[item]);
    });
    function TotalPrice(price,tonggia){
        return Number(price * tonggia).toLocaleString('en-US');
    }
    
    
    return(
        <div className="row">
            <div className="col-md-12">
            <table className="table">
                <thead>
                    <tr>
                        <th></th>
                        <th>Name</th>
                        <th>Image</th>
                        <th>Price</th>
                        <th>Quantity</th>
                        <th>Total Price</th>
                    </tr>
                </thead>
                <tbody>
                {
                    ListCart.map((item,key)=>{
                        return(
                            <tr key={key}>    
                            <td><i className="badge badge-danger" onClick={()=>DeleteCart(key)}>X</i></td>
                            <td>{item.name}</td>
                            <td><img src={item.image} style={{width:'100px',height:'80px'}}/></td>
                            <td>{item.price} $</td>
                            <td>
                                    <span className="btn btn-primary" style={{margin:'2px'}} onClick={()=>DecreaseQuantity(key)}>-</span>
                                    <span className="btn btn-info">{item.quantity}</span>
                                    <span className="btn btn-primary" style={{margin:'2px'}} onClick={()=>IncreaseQuantity(key)}>+</span>
                            </td>
                            <td>{ TotalPrice(item.price,item.quantity)} $</td>
                        </tr>
                        )
                    })
                        
                }
                <tr>
                    <td colSpan="5">Total Carts</td>
                    <td>{Number(TotalCart).toLocaleString('en-US')} $</td>
                </tr>
                </tbody>
              
            </table>
            </div>
        </div>
    )
}
const mapStateToProps = state =>{
  //  console.log(state)
    return{
        items:state._todoProduct
    }
}

export default connect(mapStateToProps,{IncreaseQuantity,DecreaseQuantity,DeleteCart})(Cart)

Ok, vậy ta mình đã setup được 80% rồi, giờ còn 20% nửa thôi
+ Mở file App.js trong thư mục src, cài đặt bộ định tuyến router cho các component

import React from 'react';
import {BrowserRouter as Router,Link, Route,Switch} from 'react-router-dom'
import Cart from './components/Cart';
import Header from './components/Header';
import Product from './components/Product';
function App() {
  return (
     <Router>
        <div className="container">
            <Header />
            <Switch>
               <Route path="/" exact component={Product} />
               <Route path="/carts" exact component={Cart} />
            </Switch>
        </div>
     </Router>
  );
}

export default App;

+ Cuối cùng là điều quan trọng nhất đó là file index.js trong thư mục src

import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css'
import App from './App';
import {Provider} from 'react-redux';
import store  from './stores'
ReactDOM.render(
  <Provider store = {store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Vậy là xong! Trong bài chia sẻ này, tôi nghĩ nó rất là phức tạp, đồi hỏi ta phải làm và test thử, để dể hình dung ra cách thức hoạt động của nó!
 

Tag: React
x

Ủng hộ tôi bằng cách click vào quảng cáo. Để tôi có kinh phí tiếp tục phát triển Website!(Support me by clicking on the ad. Let me have the money to continue developing the Website!)