在 React Redux 中打开每个产品时增加查看次数

Increase view count when each product is opened in React Redux

我想要的是增加每个产品的数量,当它被打开(查看)时,使用 react redux。

AllProductsPage.js(页面从这里开始)

import React, { useState } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { Link } from "react-router-dom";
import ProductList from "./ProductList";
import Pagination from './Pagination'
import * as productActions from "../redux/actions/productActions";
import * as userActions from '../redux/actions/userActions'
import { Button } from "react-bootstrap";
import {FiSearch} from 'react-icons/fi'
import { Container, Row, Col} from "react-bootstrap";

const AllProductsPage =(props)=> {

    const [quantity, showQuantity] = useState(true);
    const [price, showPrice] = useState(true);
    const [manufacturer,showManufacturer] = useState(true);
    const data = {quantity,price,manufacturer};
    const [search,setSearch]=useState("");
    const loggedIn = props.loggedIn;
   
    //Pagination Logic
    const [currentPage,setCurrentPage] = useState(1)
    const postsPerPage = 9
    const indexOfLastPost = currentPage * postsPerPage;
    const indexOfFirstPost = indexOfLastPost - postsPerPage;
    const currentPosts = props.products.slice(indexOfFirstPost,indexOfLastPost)

    //Change the page
    const paginate =(pageNumber)=>{
      setCurrentPage(pageNumber)
    }


     //const filteredSearch = props.products && props.products.filter(product=>product.name.toLowerCase().indexOf(search.toLowerCase())!==-1).sort( (a,b)=>(a.id>b.id)?1:-1 );
    const filteredSearch = currentPosts && currentPosts.filter(product=>product.name.toLowerCase().indexOf(search.toLowerCase())!==-1).sort( (a,b)=>(a.id>b.id)?1:-1 );

    return (
      <div>

        <div style={{"display":"flex","paddingTop":"30px"}} className="container">
         { loggedIn && <Link to="/addProduct"><Button variant="primary">Add Product</Button>{" "}</Link> }

          <span style={{"marginLeft":"auto"}}><input type="text" onChange={event=>setSearch(event.target.value)}/> {" "} <FiSearch size="20px"/> </span>
        </div>

        <div style={{"display":"flex","justifyContent":"flex-end","alignItems":"space-between","paddingTop":"6px"}} className="container" >
          <label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox"  defaultChecked={quantity} onClick={()=>showQuantity(!quantity)}/>{" "}Quantity</label>
          <label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox"  defaultChecked={price} onClick={()=>showPrice(!price)}/>{" "}Price </label>
          <label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox"  defaultChecked={manufacturer} onClick={()=>showManufacturer(!manufacturer)}/>{" "}Manufacturer </label>
        </div>
        
        <hr></hr>

        <div style={{minHeight:"100vh"}}>
        <ProductList 
          products={filteredSearch} 
          data={data} 
          togglePrice={showPrice}
          toggleQuantity={showQuantity}
          toggleManufacturer={showManufacturer}
          loggedIn={props.loggedIn}
        />
        <br />
        <Container>
          <Row>
          <Col></Col>
          <Col xs="auto" sm="auto" md="auto" lg="auto">
            <Pagination postsPerPage={postsPerPage} totalPosts={props.products.length} paginate={paginate} />
          </Col>
          <Col></Col>
          </Row>
        </Container>
        </div>
        <footer>
          <p style={{"textAlign":"center","backgroundColor":"#333","color":"white","padding":"20px"}}>Copyright @2020, Rohit K F</p>
        </footer>
      </div>
    );
}

function mapStateToProps(state, ownProps) {
  return {
    products: state.products,
    users : state.users
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(productActions, dispatch),
    userAction : bindActionCreators(userActions,dispatch)
  };
}
export default (connect(mapStateToProps, mapDispatchToProps))(AllProductsPage);

ProductList.js(然后它获取每个产品并将其传递给Product.js)

import React from "react";
import Product from "./Product";
import { Container, Row, Col} from "react-bootstrap";

const chunk = (arr, chunkSize = 1, cache = []) => {
  const tmp = [...arr]
  if (chunkSize <= 0) return cache
  while (tmp.length) cache.push(tmp.splice(0, chunkSize))
  return cache
}

const ProductList = (props) => {
  const productsChunks = chunk(props.products, 3)
  
  const rows = productsChunks.map((productChunk, index) => {
        const productsCols = productChunk.map((product, index) => {
          return (
            <Col xs="auto" sm="auto" md="auto" lg="auto" key={product.id} style={{"paddingBottom":"20px"}}>
              <Product 
              key={product.id} 
              id={product.id}
              quantity={product.quantity} 
              price={product.price} 
              name={product.name} 
              description={product.description}
              manufacturer={product.manufacturer}
                {...props}
              />      
            </Col>
          );
        });
    return (
      <Row key={index} style={{"paddingBottom":"20px"}}>
       {productsCols}
      </Row>
            
  )});
    return (
    <Container>
      {rows}
    </Container>
  )
}

export default ProductList;

Product.js(这里我们展示每个产品)

import React,{useState} from "react";
import { Link } from "react-router-dom";
import { Prompt, withRouter } from "react-router";
import { connect } from "react-redux";
import * as productActions from "../redux/actions/productActions";
import { bindActionCreators } from "redux";
import { Card, Button } from "react-bootstrap";
import toastr from "toastr";
import EditProduct from './EditProduct'
import {MdDelete,MdVisibility,MdCreate} from 'react-icons/md'


const Product = (props) => {
  const [show, setShow] = useState(false);
  const handleClose     = () => setShow(false);
  const handleShow      = () => setShow(true);
  
  const isLoggedIn = props.loggedIn
  const checkUser = (e) => {
      if (!isLoggedIn) {
        e.preventDefault();
        toastr.options = { positionClass: "toast-top-full-width",hideDuration: 300,timeOut: 2000,};
        toastr.clear();
        setTimeout(() => toastr.warning("Login to view details"), 0);
      }
  };

  const deleteProduct = () => {
    props.actions.deleteProduct(props.id)
  };
  //<Link to={'/ProductDetail/'+props.id}  >

  const product = {
    id :props.id,name:props.name,quantity:props.quantity,description:props.description,manufacturer:props.manufacturer,price:props.price
  }

  return (
    <>
    <Card style={{ width: "18rem", "borderRadius":"30px","border":"3px solid" }}>
      {isLoggedIn && (
        <Prompt when={isLoggedIn}
          message={(location) => location.pathname.includes("/ProductDetail/") ? `Are you sure you want to view the details ?` : true }
        />
      )}
      <Card.Body>
        <Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.name} </Card.Title>
        {props.data.quantity && ( <Card.Text> Quantity : {props.quantity} </Card.Text> )}
        {props.data.manufacturer && <Card.Text> Manufacturer : {props.manufacturer}</Card.Text>}
        {props.data.price && <Card.Text>$ {props.price}</Card.Text>}

        <div style={{ display: "flex", justifyContent: "space-around" }}>
          

          <Link
          to={{
            pathname: `/ProductDetail/${props.id}`,
            productName: {
              id: props.id,
              name: props.name,
              price: props.price,
              quantity: props.quantity,
              description: props.description,
              manufacturer: props.manufacturer,
            },
          }}
        >
            <Button variant="primary" onClick={(event) => checkUser(event)} style={{ "fontWeight":"bold" }} > 
                {!isLoggedIn && <span style={{"paddingRight":"5px"}}>View</span> }
                {!isLoggedIn && <MdVisibility color="black"/> }
                {isLoggedIn && <MdVisibility/>}
            </Button>
          </Link>
          {isLoggedIn &&   <Button variant="success" style={{"fontWeight":"bold"  }} onClick={() => handleShow()} ><MdCreate/></Button> }    
          {isLoggedIn &&     <Button variant="danger" style={{"fontWeight":"bold"  }} onClick={() => deleteProduct()} ><MdDelete/> </Button>}
             
        </div>
      </Card.Body>
    </Card>
    <EditProduct show={show} handleClose={handleClose} actions={props.actions} product={product}/>
    </>
  );
};
function mapStateToProps(state, ownProps) {
  return {
    products: state.products,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(productActions, dispatch),
  };
}

export default connect(mapStateToProps,mapDispatchToProps)(withRouter(Product));

ProductDetail.js(点击查看会进入该页面查看商品详情)

import React from 'react';
import { Link} from 'react-router-dom';
import {withRouter} from 'react-router'
import {Button, Card} from 'react-bootstrap'


const ProductDetail=(props)=>{
    console.log(props)
    const style={"display":"flex", "justifyContent":"center","alignItems":"center"}
    return(
            <div style={style}>
                <Card style={{ width: "18rem","borderRadius":"30px" }}>
                    <Card.Body style={{style}}>
                        <Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.location.productName.name} </Card.Title>
                        <Card.Text><strong>Quantity    :</strong>{props.location.productName.quantity}</Card.Text>
                        <Card.Text><strong>Price       :</strong>{props.location.productName.price}</Card.Text>
                        <Card.Text><strong>Manufacturer:</strong>{props.location.productName.manufacturer}</Card.Text>
                        <Card.Text><strong>Description :</strong>{props.location.productName.description}</Card.Text>
                        <div>
                        <Link to="/"><Button variant="primary" style={{ height: "6vh","fontWeight":"bold" }}>Back</Button></Link>
                        </div>
                    </Card.Body>
                </Card>
            </div>
        );
}
export default withRouter(ProductDetail); 

ProductReducer.js

import initialState from "./initialState";
import * as actionTypes from "../actions/actionTypes";

export default function productReducer(state = initialState.products, action) {
  switch (action.type) {
    case actionTypes.INIT:
      return action.products;

    case actionTypes.ADD:
      return [...state, Object.assign({}, action.product)];

    case actionTypes.DELETE:
      return [...state.filter((product) => product.id !== action.id)];

    case actionTypes.UPDATE:
      return [
        ...state.filter((product) => product.id !== action.product.id),
        Object.assign({}, action.product),
      ];

    case actionTypes.VIEW:
      return [
        ...state[action.product.id],
        Object.assign({},action.product.view)
      ]

    default:
      return state;
  }
}

ProductActions.js

import dataApi from "../../server/dataAPI";
import * as actionTypes from "../actions/actionTypes";

//======================LOADING A PRODUCT
export function loadProduct() {
  return function (dispatch) {
    return dataApi
      .getAllProducts()
      .then((products) => {
        dispatch({ type: actionTypes.INIT, products });
      })
      .catch((error) => {
        throw error;
      });
  };
}
//==========================ADDING A PRODUCT
export function addProduct(product) {
  return function (dispatch) {
    return dataApi
      .addProduct(product)
      .then((product) => {
        dispatch({ type: actionTypes.ADD, product });
      })
      .catch((error) => {
        throw error;
      });
  };
}

//==========================DELETE A PRODUCT
export function deleteProduct(id) {
  return function (dispatch) {
    return dataApi
      .deleteProduct(id)
      .then((product) => {
        dispatch({ type: actionTypes.DELETE, id});
      })
      .catch((error) => {
        throw error;
      });
  };
}

//==========================UPDATE A PRODUCT
export function updateProduct(product) {
    return function (dispatch) {
      return dataApi
        .updateProduct(product)
        .then((product) => {
          dispatch({ type: actionTypes.UPDATE, product });
        })
        .catch((error) => {
          throw error;
        });
    };
  }

  //Increase View Count of product
  export function addView(product){
    return function (dispatch){
      return dataApi.addView(product)
      .then(product=>{
        dispatch({type:actionTypes.VIEW, product})
      })
    }
  }

dataAPI.js(使用 axios 添加、删除、更新到 json 服务器)

import axios from 'axios'

class dataAPI {
    static  getAllProducts() {
        return axios.get('http://localhost:4000/products?_sort=id&_order=asc').then(response=>response.data);
    }

    static addProduct(product) {
        return axios.post('http://localhost:4000/products',product).then(response=>response.data);
    }
    
    static updateProduct(product){
        return axios.patch('http://localhost:4000/products/'+product.id,product)
        .then(response=>response.data);
    }

    static deleteProduct(id){
        return axios.delete(`http://localhost:4000/products/${id}`).then(response=>response.data);
    }

    static getAllUsers(){
        return axios.get('http://localhost:4000/users').then(response=>response.data);
    }

    static addUser(user) {
        return axios.post('http://localhost:4000/users',user).then(response=>response.data);
    }
}

export default dataAPI;

db.json(包含所有数据的文件)

{
  "products": [
    {
      "id": 1,
      "name": "Moto G5 Ultra",
      "quantity": 3,
      "price": 10000,
      "description": "Moto G5",
      "manufacturer": "Motorola",
      "views" : 0
    },
    {
      "id": 2,
      "name": "Racold Geyser",
      "quantity": 2,
      "price": 60000,
      "description": "Moto G5",
      "manufacturer": "Motorola",
      "views" : 0
    },
    {
      "name": "Lenovo G5",
      "quantity": 3,
      "price": 55000,
      "manufacturer": "Lenovo",
      "description": "A gaming laptop",
      "id": 3,
      "views" : 0
    },
    {
      "name": "Acer Swift ",
      "quantity": 5,
      "price": 35000,
      "manufacturer": "Acer",
      "description": "Business Laptop",
      "id": 4,
      "views" : 0
    },
    {
      "name": "Acer Nitro 7",
      "quantity": 4,
      "price": 75000,
      "manufacturer": "Acer",
      "description": "A gaming laptop",
      "id": 5,
      "views" : 0
    },
    "users": [
    {
      "id": 1,
      "email": "vi@gmail.com",
      "password": "truth",
      "name": {
        "firstName": "Rick",
        "lastName": "Garner"
      },
      "location": "Canada",
      "mobile": "55643980"
    },
    {
      "id": 2,
      "email": "t@t.com",
      "password": "123",
      "name": {
        "firstName": "Ram",
        "lastName": "Shankar"
      },
      "location": "Delhi",
      "mobile": "9895454860"
    },
    {
      "email": "e@e.com",
      "password": "123456789",
      "name": {
        "firstName": "RAGAV",
        "lastName": "Shant"
      },
      "location": "Karnataka",
      "mobile": "1234567891",
      "id": 3
    },
    {
      "email": "k@k.com",
      "password": "123456789",
      "name": {
        "firstName": "sd",
        "lastName": "dv"
      },
      "location": "dfv",
      "mobile": "12345678231",
      "id": 4
    }
    
  ]
}

您可能希望在 ProductDetail.jsx 页面内的 useEffect 中发送更新产品操作。

useEffect(() => {
  updateProduct({
    ...props.location.productName,
    views: props.location.productName + 1,
  });
}, []);

当然你还需要从Product.jsx传递views。 这将增加用户每次 opens/refreshes 页面的浏览量。

编辑:

如果您想要单独的 API 端点来增加观看次数,您可以在服务器端实现其增加逻辑。在这种情况下,它不会更改当前减速器文件 ProductReducer.js 中的任何内容。 但我认为没有必要。出于这个原因,您可以使用 updateProduct API 。在这种情况下也不需要更换减速器。

编辑 2:

如果 addView API 返回产品 ID 和增量视图,那么你可以将 reducer 写成 -

case actionTypes.VIEW:
  return [
    ...state.map((product) => {
      if (product.id === action.product.id) {
        product.views = action.product.views;
      }
      return product;
    })
  ]

所以我所做的是在我的 ProductDetail.js 文件中添加了一个 useEffect() 并从那里启动了 Action。

ProductDetail.js

import React,{useEffect} from 'react';
import { Link} from 'react-router-dom';
import {withRouter} from 'react-router'
import {Button, Card} from 'react-bootstrap'
import { connect } from "react-redux";
import * as productActions from "../redux/actions/productActions";
import { bindActionCreators } from "redux";


const ProductDetail=(props)=>{
    useEffect(() => {

        console.log("PROPIES ",props.location.productName.id+" "+props.location.productName.views)
        props.actions.addView(props.location.productName.id,props.location.productName.views)
    
    },[props.actions,props.location.productName.id,props.location.productName.views])
    const style={"display":"flex", "justifyContent":"center","alignItems":"center","minHeight":"100vh"}
    return(
            <div style={style}>
                <Card style={{ width: "18rem","borderRadius":"30px" }} >
                    <Card.Body style={{style}}>
                        <Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.location.productName.name} </Card.Title>
                        <Card.Text><strong>Quantity    :</strong>{props.location.productName.quantity}</Card.Text>
                        <Card.Text><strong>Price       :</strong>{props.location.productName.price}</Card.Text>
                        <Card.Text><strong>Manufacturer:</strong>{props.location.productName.manufacturer}</Card.Text>
                        <Card.Text><strong>Description :</strong>{props.location.productName.description}</Card.Text>
                        <div>
                        <Link to="/"><Button variant="primary" style={{ height: "6vh","fontWeight":"bold" }}>Back</Button></Link>
                        </div>
                    </Card.Body>
                </Card>
            </div>
        );
}

function mapStateToProps(state, ownProps) {
    return {
      products: state.products,
    };
  }
  
  function mapDispatchToProps(dispatch) {
    return {
      actions: bindActionCreators(productActions, dispatch),
    };
  }

export default connect(mapStateToProps,mapDispatchToProps)(withRouter(ProductDetail)); 

然后触发这个动作

//Increase View Count of product
  export function addView(id,count){
    console.log("func called")
    return function (dispatch){
      console.log("api to be called")
      return dataApi.addView(id,count)
      .then((product)=>{
        console.log("dispatched")
        dispatch({type:actionTypes.VIEW, id: product.id})
      })
    }
  }

所以它先更新服务器上的视图,然后在reducer状态下更新

dataAPI.js

static addView(id,count){
      return axios.patch('http://localhost:4000/products/'+id,{views:count+1})
        .then(response=>response.data);
    }

productReducer.js

import initialState from "./initialState";
import * as actionTypes from "../actions/actionTypes";

export default function productReducer(state = initialState.products, action) {
  switch (action.type) {
    case actionTypes.INIT:
      return action.products;

    case actionTypes.ADD:
      return [...state, Object.assign({}, action.product)];

    case actionTypes.DELETE:
      return [...state.filter((product) => product.id !== action.id)];

    case actionTypes.UPDATE:
      return [
        ...state.filter((product) => product.id !== action.product.id),
        Object.assign({}, action.product),
      ].sort( (a,b)=>(a.id>b.id)?1:-1 );

    case actionTypes.VIEW:
      let prod = [...state][action.id-1];
      prod.views++;
      //eslint-disable-next-line
      let addView =()=>( [
           ...state.filter(product => product.id !== action.id),
           Object.assign({}, prod)
         ])
      return state;

    default:
      return state;
  }
}

我不得不像这样在 switch 中编写 ActionType.VIEW 案例

case actionTypes.VIEW:
      let prod = [...state][action.id-1];
      prod.views++;
      //eslint-disable-next-line
      let addView =()=>( [
           ...state.filter(product => product.id !== action.id),
           Object.assign({}, prod)
         ])
      return state;

我不得不将状态修改部分放在一个名为 addView() 的函数中,否则我会看到该函数被无限地重复调用。如果有人可以帮我解决这个问题,我将不胜感激