MERN-Stack Shopping Cart - Part 2

Learning Objectives
Students Will Be Able To: |
---|
Implement "Shopping Cart" Functionality |
Change Client-Side Routes Programmatically |
Road Map
- Setup
- Review MERN-Stack CRUD Logic & Code Flow
- Adding Items to the Cart
- Changing the Quantity Ordered
- Checking Out an Order
- Programmatic Routing Using the
useNavigate
Hook
Videos
1. Setup
The code for this lesson begins right where we left off in the MERN-Stack Shopping Cart - Part 1 lesson.
-
Move into the
sei-cafe
project folder:cd ~/code/sei-cafe
There's no need to sync your code unless your code from Part 1 is not complete, otherwise...
👀 Do you need to sync your code?
git reset --hard origin/sync-cafe-10-shop-2-starter
-
Open VS Code:
code .
-
Open a terminal and start the Express server:
nodemon server
-
Open another terminal and start React's dev server:
npm start
2. Review MERN-Stack CRUD Logic & Code Flow
Prior to implementing adding items to the cart in the next step, let's review the typical logic and code flow when performing CRUD in the MERN-Stack...

Note: The code above is not meant to be complete.
FEATURE A - Load & Display Posts Upon First Render
STEP | DESCRIPTION |
---|---|
A1 | After the <PostListPage> has rendered for the first time, the useEffect function runs calling postsAPI.getAll() in the posts-api.js API module. |
A2 | The getAll() function delegates making the AJAX request by calling the sendRequest() function. |
A3 & A4 | The sendRequest() function uses the browser's fetch function to send the AJAX request to the server where the request flows through the Express app's middleware until it matches the route. |
A5 | The route calls the postsCtrl.getAll() controller action which uses the Post model to retrieve all posts for the logged in user. |
A6 | The controller action responds to the AJAX request using res.json(posts) sending back an array of the user's posts - completing the request initiated by postsAPI.getAll() . The connecting line is dashed because the posts actually flow back through the fetch() , sendRequest() , postsAPI.getAll() functions. |
FEATURE B - Create Post When Form is Submitted in Child Component
STEP | DESCRIPTION |
---|---|
B1 | The user submits the form in <PostForm> which causes its handleSubmit event handler to execute. |
B2 | The event handler, after preventing the default action of the form being submitted to the server, calls the handleAddPost() function passed to it as a prop from <PostListPage> with an argument of the data for the new post (content ). |
B3 | The handleAddPost() function calls postsAPI.add(postData) in the posts-api.js API module. |
B4 | The add() function in posts-api.js delegates making the AJAX request by calling the sendRequest() function. |
B5 & B6 | The sendRequest() function uses the browser's fetch function to send the AJAX request to the server where the request flows through the Express app's middleware until it matches the route. |
B7 | The route calls the postsCtrl.create() controller action which uses the Post model to create the user's new post. |
B8 | The controller action responds to the AJAX request using res.json(post) sending back the user's new post - completing the request initiated by postsAPI.add() . The connecting line is dashed because the post actually flows back through the fetch() , sendRequest() , postsAPI.add() functions. |
Hungry?...
3. Adding Items to the Cart
If we take a look we'll see that <OrderDetail>
is already mapping the order's line items into an array of <LineItem>
components to be rendered in its JSX:
const lineItems = order.lineItems.map((item) => (
<LineItem lineItem={item} isPaid={order.isPaid} key={item._id} />
));
❓ Why is the isPaid
prop there? In other words, why would a line item need to know if the order is paid or not? Browse to the deployed app for a hint - be sure to have at least one item in your cart.
isPaid
prop there? In other words, why would a line item need to know if the order is paid or not? Browse to the deployed app for a hint - be sure to have at least one item in your cart.<LineItem>
should not allow the quantity to be changed if the order is already paid - so it should not render the [-]
and [+]
buttons.
Adding Items - Start with the UI
Each <MenuListItem>
component is already rendering an [ADD] button that console.logs when clicked, so our work is done here...
Stub Up a handleAddToOrder
Function
When a menu item is added to the cart, we'll need to:
- Make an AJAX request to add the item.
- Update the order in the controller action on the server.
- Code the controller action to respond with the updated order.
- Update the
cart
state with the updated order.
Because the cart
state is in <NewOrderPage>
, and we need to do more than just update that state, <NewOrderPage>
is where we should handle the click event of an [ADD] button:
...
/*--- Event Handlers --- */
async function handleAddToOrder(itemId) {
// Baby step
alert(`add item: ${itemId}`);
}
return (
...
👉 You Do - handleAddToOrder
(4 minutes)
- Pass the
handleAddToOrder
function as a prop of the same name through the component hierarchy to the<MenuListItem>
component. - In the
<button>
of<MenuListItem>
invokehandleAddToOrder
with an argument ofmenuItem._id
instead of theconsole.log('clicked')
. - Verify that the alert displays with the item's id when the [ADD] button is clicked.

Adding Items - The Remaining Flow
Here's the remaining flow of logic when an [ADD] button is clicked:
-
Make an AJAX request that lets the server know that we want to add a menu item to the user's cart. There's already an
addItemToCart
function ready for action in orders-api.js. -
A route has already been defined on the server to listen for the AJAX request:
router.post("/cart/items/:id", ordersCtrl.addToCart);
-
The
addToCart
controller function mapped to by the route is stubbed up, however, we still need to write the code to update the user's cart and respond with the updated cart.
Finish Coding the handleAddToOrder
Function
Not much to do, so give it a shot...
👉 You Do - Code handleAddToOrder
(2 minutes)
-
Finish the
handleAddToOrder
function in NewOrderPage.jsx:pages/NewOrderPage/NewOrderPage.jsxasync function handleAddToOrder(itemId) {
// alert(`add item: ${itemId}`);
// 1. Call the addItemToCart function in ordersAPI, passing to it the itemId, and assign the resolved promise to a variable named cart.
// 2. Update the cart state with the updated cart received from the server
}
Add an addItemToCart
Instance Method to the orderSchema
Mongoose schema instance methods are callable on documents - what a great place to add the logic for adding an item to a cart:
...
// Instance method for adding an item to a cart (unpaid order)
orderSchema.methods.addItemToCart = async function (itemId) {
// this keyword is bound to the cart (order doc)
const cart = this;
// Check if the item already exists in the cart
const lineItem = cart.lineItems.find(lineItem => lineItem.item._id.equals(itemId));
if (lineItem) {
// It already exists, so increase the qty
lineItem.qty += 1;
} else {
// Get the item from the "catalog"
// Note how the mongoose.model method behaves as a getter when passed one arg vs. two
const Item = mongoose.model('Item');
const item = await Item.findById(itemId);
// The qty of the new lineItem object being pushed in defaults to 1
cart.lineItems.push({ item });
}
// return the save() method's promise
return cart.save();
};
Good stuff in there with lots of comments.
Code the addToCart
Controller Action
All that's left is to code the addToCart
controller action:
// Add the item to the cart
async function addToCart(req, res) {
const cart = await Order.getCart(req.user._id);
// The promise resolves to the document, which we already have
// in the cart variable, so no need to create another variable...
await cart.addItemToCart(req.params.id);
res.json(cart);
}
Again, skinny controllers, fat models.
My personal fav:

👀 Do you need to sync your code?
git reset --hard origin/sync-cafe-11-shop-2-add-item
4. Changing the Quantity Ordered
As you can see, each <LineItem>
is rendering [-]
and [+]
buttons - but we need to implement their functionality.
Implementing this functionality is very similar to what we just did, so forgive me if I enter ninja mode as we enthusiastically write the following code...
Code the handleChangeQty
Function
The handleChangeQty
function belongs in <NewOrderPage>
just like handleAddToOrder
we just coded:
/*--- Event Handlers --- */
async function handleAddToOrder(itemId) {
const updatedCart = await ordersAPI.addItemToCart(itemId);
setCart(updatedCart);
}
// Add this function
async function handleChangeQty(itemId, newQty) {
const updatedCart = await ordersAPI.setItemQtyInCart(itemId, newQty);
setCart(updatedCart);
}
The setItemQtyInCart
function has already been coded in orders-api.js.
Now invoke it from the UI...
Invoke the handleChangeQty
Function
We need to:
- Pass
handleChangeQty
down thru the hierarchy to the<LineItem>
component - let's do it and don't let me forget to destructure props all the way down! - Invoke it in the existing
onClick
arrow functions in both the[-]
and[+]
buttons. Looking at the signature ofhandleChangeQty
, we see that it expects theitemId
and thenewQty
- let's oblige with the following refactor:
...
<div className="qty" style={{ justifyContent: isPaid && 'center' }}>
{!isPaid &&
<button
className="btn-xs"
// Refactor
onClick={() => handleChangeQty(lineItem.item._id, lineItem.qty - 1)}
>−</button>
}
<span>{lineItem.qty}</span>
{!isPaid &&
<button
className="btn-xs"
// Refactor
onClick={() => handleChangeQty(lineItem.item._id, lineItem.qty + 1)}
>+</button>
}
</div>
...
That does it on the client - the ninja is on the way to the server...
Add the setItemQty
Instance Method to the orderSchema
The setItemQty
instance method is very similar to the addItemToCart
we coded a bit ago:
// Instance method to set an item's qty in the cart (will add item if does not exist)
orderSchema.methods.setItemQty = function (itemId, newQty) {
// this keyword is bound to the cart (order doc)
const cart = this;
// Find the line item in the cart for the menu item
const lineItem = cart.lineItems.find((lineItem) =>
lineItem.item._id.equals(itemId)
);
if (lineItem && newQty <= 0) {
// Calling deleteOne(), removes itself from the cart.lineItems array
// Note that video shows remove(), which has been removed 😀 in Mongoose v7
lineItem.deleteOne();
} else if (lineItem) {
// Set the new qty - positive value is assured thanks to prev if
lineItem.qty = newQty;
}
// return the save() method's promise
return cart.save();
};
module.exports = mongoose.model("Order", orderSchema);
Now let's put it to use...
Code the setItemQtyInCart
Controller Action
Another clean controller action coming up:
// Updates an item in the cart's qty
async function setItemQtyInCart(req, res) {
const cart = await Order.getCart(req.user._id);
await cart.setItemQty(req.body.itemId, req.body.newQty);
res.json(cart);
}
Now we're talking!

👀 Do you need to sync your code?
git reset --hard origin/sync-cafe-12-shop-2-qty
5. Checking Out an Order
One last feature!
AAU, I want to click a [CHECKOUT] button that pays the order and sends me to the Order History Page.
Ninja chop!
Client-Side Code
More of the same, well almost:
...
async function handleCheckout() {
await ordersAPI.checkout();
navigate('/orders');
}
return (
...
We'll discuss the navigate('/orders')
in a bit, but let's first pass handleCheckout
to the <OrderDetail>
component.
Now we can invoke it:
...
<button
className="btn-sm"
onClick={handleCheckout}
disabled={!lineItems.length}
>CHECKOUT</button>
...
No reason to wrap it with an arrow function - you know why, right?
Onto that navigate('/orders');
business...
6. Programmatic Routing Using the useNavigate
Hook
There will certainly be times when need to change client-side routes programmatically, i.e., using code, instead of in response to the user clicking a <Link>
.
React Router makes changing client-side routes easy with its useNavigate
hook...
First we need to import it:
// Update this import
import { Link, useNavigate } from "react-router-dom";
The useNavigate
hook is a function like all hooks are - invoking it returns a navigate
function:
...
const categoriesRef = useRef([]);
// Use the navigate function to change routes programmatically
const navigate = useNavigate();
...
To change client-side routes, we just invoke the navigate
function and provide it the path of where you want to go to like we just did to switch to the /orders
path above:
navigate("/orders");
We're done on the client, and not far from being done on the server...
Code the checkout
Controller Action
Not much logic necessary - all we have to do is update the cart document's isPaid
property to true
- so we'll forgo adding a new method to the schema and just put the logic in the controller action:
// Update the cart's isPaid property to true
async function checkout(req, res) {
const cart = await Order.getCart(req.user._id);
cart.isPaid = true;
await cart.save();
res.json(cart);
}
👀 If your future e-commerce apps have additional logic, be sure to code that logic on the model whenever possible.
Payments
If you need to implement payments for an e-commerce site in the future, a popular to check out is stripe.
Good work hanging in there
👀 Do you need to sync your code?
git reset --hard origin/sync-cafe-13-shop-2-finish