Search code examples
reactjsreact-reduxreact-routerreact-router-domreact-router-redux

No routes matched location for cartItems app


There is a routing problem with using react-router-dom v6 to maintain "cart items" functionality in my app. So it doesn't show cart items when I click on the "cart" link in the header navbar. In contrast, it works and displays the list when I try to add a new item to the cart.

Note: It shows the cart items when the URL path is such this pattern 'http://localhost:3000/cart/1?qty=2' and doesn't show in the case of 'http://localhost:3000/cart'!

Please follow the code...

App.js

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import HomeScreen from './screens/HomeScreen';
import CartScreen from './screens/CartScreen';
import ProductScreen from './screens/ProductScreen';
 
function App() {
  return (
    <>
      <Header/>
      <Routes>
        <Route path="/" element={<HomeScreen />} exact />
        <Route path="/product/:id" element={<ProductScreen />} />
        <Route path="/cart/:productid?" element={<CartScreen />}/>
      </Routes>
    );
  </>
}

👆 I have configured all the routes, and if you focus on the cart path we add '/cart/' followed by ': productid?' as a product parameter

ProductScreen.js

import { useParams, Link, useNavigate } from 'react-router-dom'; 
import { useDispatch, useSelector } from 'react-redux';
import {Button} from 'react-bootstrap';

function ProductScreen() {
  const { id } = useParams(); // the product 'id'
  const navigate = useNavigate();
  const [qty, setQty] = useState(1); // the product quantity to be added on cart
  const dispatch = useDispatch();
  const productListDetail = useSelector((state) => state.productDetail);
  const { loading, error, product } = productListDetail;
  
  useEffect(() => {
    dispatch(listProductDetails(id));
  }, [dispatch, id]);

  const addToCartHandler = () => {
    navigate(`/cart/${id}?qty=${qty}`);
  };

  return (
    <ListGroup> 
      <ListGroup.Item>
        <Row>
          <Col> Qty </Col>
          <Col xs="auto" className="my-1"> 
            <Form.Control
              as="select"
              value={qty} 
              onChange={(e) => setQty(e.target.value)}
            >
              {[...Array(product.countInStock).keys()]
                .map((x) => (
                  <option key={x + 1} value={x + 1}> {x + 1} </option>
                )
              )}
            </Form.Control>
          </Col>
        </Row>
      </ListGroup.Item>
      <ListGroup.Item> 
        <Button onClick={addToCartHandler} type="button"> Add to Cart </Button> 
      </ListGroup.Item>
    </ListGroup>
  )
}

👆here when you click on 'Add to Cart' button, it will add the item and navigate to the cart list view.

Header.js

import React from 'react';
import { Navbar, Nav, Container } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';

function Header() {
  return (
    <header >
      <Navbar bg="dark" variant="dark" expand="lg" collapseOnSelect>
        <Container>
          <LinkContainer to="/cart">
            <Nav.Link>
              <i className="fas fa-shopping-cart"></i>
              Cart
            </Nav.Link>
          </LinkContainer>
        </Container>
      </Navbar>
    </header>
  );
}
export default Header;

👆Here when the user clicks on the 'cart' link, it must navigate to and show all stored items in the cart list, but shows nothing and in the browser's console it warns 'router.ts:11 No routes matched location "/cart/"' message as shown below.

enter image description here

cartScreen.js

import React, { useEffect } from 'react';
import { Col,ListGroup,Row,Image,Button,Card,Form} from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useNavigate, useParams, Link, Outlet } from 'react-router-dom';
import { addToCart } from '../actions/cartAction';
import Message from '../components/Message';

const CartScreen = () => {
  // "useLocation" Returns the current location object, which represents the current URL in web browsers.
  const { search } = useLocation();

  const { productid } = useParams();

  const qty = search ? Number(search.split('=')[1]) : 1;
  

  const dispatch = useDispatch();

  const cart = useSelector((state) => state.cart);

  const { cartItems } = cart;

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

  return (
  
    <Row>
      <Col md={8}>
        {cartItems.length === 0 ? (
          <Message variant="info">
            {' '}
            Go Back To Home Page <Link to="/"></Link>
          </Message>
        ) : (
          <ListGroup>
            {cartItems.map((x) => (
              <ListGroup.Item key={x.product}>
                {x.name} , {x.qty}
              </ListGroup.Item>
            ))}
          </ListGroup>
        )}
      </Col>
      <Col md={4}></Col>
    </Row>
   
  );
};

export default CartScreen;


Solution

  • It seems you want the CartScreen component to render on both the "/cart/:productid" path (in the case of updating the cart) and also on the "/cart" path when just viewing the cart.

    For this you need to render a Route for each. Here I've structured a layout route on path="/cart" that renders two nested routes: an index route rendering a CartScreen on "." and another CartScreen on ":productid".

    <Routes>
      <Route path="/" element={<HomeScreen />} />
      <Route path="/product/:id" element={<ProductScreen />} />
      <Route path="/cart">
        <Route path=":productid" element={<CartScreen />} />
        <Route index element={<CartScreen />} />
      </Route>
    </Routes>
    

    Note: You could equally render two routes individually we well if desired.

    <Routes>
      <Route path="/" element={<HomeScreen />} />
      <Route path="/product/:id" element={<ProductScreen />} />
      <Route path="/cart:productid" element={<CartScreen />} />
      <Route path="/cart" element={<CartScreen />} />
    </Routes>
    

    The CartScreen should handle the queryString in a more react-router-dom@6 way. Instead of using the useLocation hook to access the location.search property, and then applying some "string logic", use the useSearchParams hook to directly access a searchParams object and get the exact queryString parameter value needed.

    Example:

    const CartScreen = () => {
      const { productid } = useParams();
      const [searchParams, setSearchParams] = useSearchParams();
      const dispatch = useDispatch();
      const { cartItems } = useSelector((state) => state.cart);
    
      const qty = searchParams.get("qty") || 1;
    
      useEffect(() => {
        if (qty) {
          // update cart state
          dispatch(addToCart(productid, qty));
    
          // clear qty queryString param, then "navigate"
          searchParams.delete("qty");
          setSearchParams(searchParams);
        }
      }, [dispatch, productid, qty, searchParams, setSearchParams]);
    
      return (
        <Row>
          <Col md={8}>
            {!cartItems.length ? (
              <Message variant="info">
                Go Back To Home Page <Link to="/"></Link>
              </Message>
            ) : (
              <ListGroup>
                {cartItems.map((x) => (
                  <ListGroup.Item key={x.product}>
                    {x.name} , {x.qty}
                  </ListGroup.Item>
                ))}
              </ListGroup>
            )}
          </Col>
          <Col md={4}></Col>
        </Row>
      );
    };