Search code examples
reactjsexpressreduxreact-router

localhost:3000/cart, got an error as localhost :3000/api/products/cart 404(Not Found)


I am working on an E-commerce website and adding a 'cart' route on it, but it got the following Error: Error image The localhost address is :3000/cart, but the error address is from :3000/api/products/cart.(I am using express to fetch data from :http://localhost:5000/api/products)

Also, if I input the address as:" http://localhost:3000/cart/223", the error came out as:

GET http://localhost:3000/cart/api/products/cart 404   (Not Found)

App.js :

function App() {
  return (
    <Switch>
      <Route exact path="/" component={HomePage} />
      <Route exact path="/products" component={ProductPage}/ >
      <Route path="/:id" component={ProductDetail} />
      <Route path="/cart" component={CartPage} />
      <Route path="/cart/:id" component={CartPage} />
    </Switch>
  )
}

export default App;

cartPage.jsx :

import React, { useEffect } from "react";
import { addToCart } from "../actions/cartActions";
import { useDispatch } from "react-redux";

function CartPage(props) {
  const productId = props.match.params.id;
  const qty = props.location.search? Number(props.location.search.split("=")[1]): 1;
  const dispatch = useDispatch();

  useEffect(() => {
    if (productId) {
      dispatch(addToCart(productId, qty));
    }
  }, []);

  return <div>Hell world!</div>;
}

export default CartPage;

productDetail.jsx:

import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { detailsProduct } from "../actions/productActions";

function ProductDetail(props) {
  const [qty, setQty] = useState(1);
  const productDetails = useSelector((state) => state.productDetails);
  const { product, loading, error } = productDetails;
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(detailsProduct(props.match.params.id));
    return () => {//};
  }, []);

  const handleAddToCart = () => {
    props.history.push("/cart/" + props.match.params.id + "?qty=" + qty);
  };

  return ({product.countInStock > 0 && (
    <button onClick={handleAddToCart}>
      Add to Cart
    </button>
  )})

http://localhost:5000/api/products:

[
  {
     name: "Slim Shirt",
     category: "Shirts",
     image: "/img/d2.png",
     price: 60,
     brand: "Nike",
     rating: 4.5,
     numReviews: 10,
     _id: "123",
     countInStock: 0,
   },
   {
     name: "Best Shirt",
     category: "Shirts",
     image: "/img/d3.png",
     price: 50,
     brand: "ads",
     rating: 4.6,
     numReviews: 12,
     _id: "223",
     countInStock: 6,
   },
   ....}

server.js:

import express from "express";
import { data } from "./data";

const app = express();

app.get("/api/products/:id", (req, res) => {
  const product = data.find((x) => x._id === req.params.id);

  if (product) {
    res.send(product);
  } else {
    res.status(404).send({ msg: "Product Not Found!!"     });
  }
});

app.get("/api/products", (req, res) => {
  res.send(data);
});

app.listen(5000, () => {
  console.log("server started at http://localhost:5000");
});

package.jason:

{
 "name": "frontend",
 "proxy": "http://127.0.0.1:5000",
 "version": "0.1.0",
 "private": true,
 "dependencies": {
    .....}

//store.js:

import { createStore, combineReducers, compose, 
applyMiddleware } from "redux";
import {
productDetailsReducer,
productListReducer,
} from "./reducers/productReducers";
import thunk from "redux-thunk";
import { cartReducer } from "./reducers/cartReducers";

const initialState = {};
const reduer = combineReducers({
productList: productListReducer,
productDetails: productDetailsReducer,
cart: cartReducer,
});

const composeEnhancer = 
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
reduer,
initialState,
composeEnhancer(applyMiddleware(thunk))
);
export default store;

//productActions.js:

const listProducts = () => async (dispatch) => {
  try {
    dispatch({ type: PRODUCT_LIST_REQUEST });
    const { data } = await axios.get("api/products");
    dispatch({ type: PRODUCT_LIST_SUCCESS, payload: data 
    });
 } catch (error) {
   dispatch({ type: PRODUCT_LIST_FAIL, payload: 
   error.message });
  }
 };

const detailsProduct = (productId) => async (dispatch) => {
  try {
   dispatch({ type: PRODUCT_DETAILS_REQUEST, payload: 
   productId });
   const { data } = await axios.get("api/products/" + 
     productId);
   dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data 
   });
   } catch (error) {
   dispatch({ type: PRODUCT_DETAILS_FAIL, payload: 
  error.message });
    }
  };

  export { listProducts, detailsProduct };

Solution

  • Issue

    The issue seems to start with the order of the routes rendered by the Switch component. Route path order and specificity matters within the Switch. The "/:id" route path is less specific than "/cart" which is less specific than "/cart/:id", and so it will be matched and rendered by the Switch.

    <Switch>
      <Route exact path="/" component={HomePage} />
      <Route exact path="/products" component={ProductPage} />
      <Route path="/:id" component={ProductDetail} /> // matches "/cart"
      <Route path="/cart" component={CartPage} />     // unreachable route!!
      <Route path="/cart/:id" component={CartPage} /> // unreachable route!!
    </Switch>
    

    The last two routes are unreachable. The wrong component is rendered when the path is "/cart" and an invalid request is made.

    ProductDetail dispatches an action that is likely meant to make a fetch request with a proper id, bu "cart" is the id value with path "/cart" matched by the "/:id" route.

    useEffect(() => {
      dispatch(detailsProduct(props.match.params.id));
      return () => {};
    }, []);
    

    Solution

    Reorder the routes in inverse order of path specificity. In most cases doing this also completely removes the need to pepper all the routes with the exact prop.

    Example:

    <Switch>
      <Route path="/cart/:id" component={CartPage} />
      <Route path="/cart" component={CartPage} />
      <Route path="/products" component={ProductPage} />
      <Route path="/:id" component={ProductDetail} />
      <Route path="/" component={HomePage} />
    </Switch>
    

    Note that "/cart/:id" is more specific than "/cart", "/products", and "/:id", and that both "/cart" and "/products" are more specific than "/:id", and they are all more specific than the last route path "/".

    Note also that react-router-dom@5 allows the Route component to use an array of paths for the path prop. Keep in mind that the order and specificity still matters within the array. Keep also in mind that all routes in the array should be more specific than the paths of routes below/after.

    Example:

    <Switch>
      <Route path={["/cart/:id", "/cart"]} component={CartPage} />
      <Route path="/products" component={ProductPage} />
      <Route path="/:id" component={ProductDetail} />
      <Route path="/" component={HomePage} />
    </Switch>
    

    API requests

    For the requests try using an absolute path, i.e. axios.get("/api/products/" + productId) so the request isn't being made from the "/products" sub-route. You want the API request path to be something like "/api/products/123" instead of "/products/api/products/123".

    Example:

    const listProducts = () => async (dispatch) => {
      try {
        dispatch({ type: PRODUCT_LIST_REQUEST });
        const { data } = await axios.get("/api/products");
        dispatch({ type: PRODUCT_LIST_SUCCESS, payload: data });
      } catch (error) {
        dispatch({ type: PRODUCT_LIST_FAIL, payload: error.message });
      }
    };
    
    const detailsProduct = (productId) => async (dispatch) => {
      try {
        dispatch({ type: PRODUCT_DETAILS_REQUEST, payload: productId });
        const { data } = await axios.get("/api/products/" + productId);
        dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data });
      } catch (error) {
        dispatch({ type: PRODUCT_DETAILS_FAIL, payload: error.message });
      }
    };
    
    export { listProducts, detailsProduct };