├── README.md
├── code
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── components
│ │ ├── Nav.js
│ │ ├── ProductList.js
│ │ ├── ProductRow.js
│ │ └── cart
│ │ │ ├── Cart.js
│ │ │ ├── CartCheckoutRow.js
│ │ │ ├── CartProductList.js
│ │ │ ├── CartProductRow.js
│ │ │ ├── CartTotalRow.js
│ │ │ └── RecommendedProduct.js
│ ├── context
│ │ └── CartContext.js
│ ├── index.css
│ ├── index.js
│ ├── lib
│ │ └── Commerce.js
│ ├── logo.svg
│ ├── logo192.png
│ ├── serviceWorker.js
│ └── setupTests.js
└── yarn.lock
└── images
├── cart_button.png
├── cart_recommended_product.png
├── cart_with_products.png
├── product_list.png
└── recommended_add.png
/README.md:
--------------------------------------------------------------------------------
1 | # Adding products to the cart with React.js and Commerce.js
2 |
3 | ## Overview
4 |
5 | In the previous guide, [Listing products with React.js and Commerce.js](https://github.com/robingram/commercejs-list-products-react) we created a simple list of the products we'd created in the Chec Dashboard. Next we're going to look at how to display the contents of the cart and add those products to it. Finally, we'll add simulated recommended products to the cart to encourage further purchases.
6 |
7 | In this guide we'll:
8 |
9 | * Retrieve a cart from Commerce.js and make it available to our app
10 | * Add React components to display the contents of the cart
11 | * Extend the existing products list to allow products to be added to the cart
12 | * Display simulated recommended products products in the cart
13 |
14 | You can see a [live demo](http://commercejs-add-to-cart.s3-website-ap-southeast-2.amazonaws.com/) of what we'll produce.
15 |
16 | ## Requirements
17 |
18 | As with the [previous guide](https://github.com/robingram/commercejs-list-products-react), you'll need a [Chec dashboard](https://authorize.chec.io/login) account, Node.js and npm/yarn and a code editor.
19 |
20 | Some basic knowledge of HTML, JavaScript and Bootstrap would be helpful and you'll need to be comfortable using the command line on your computer.
21 |
22 | ## Setup
23 |
24 | We'll build on the code from the previous guide but since this time we'll be making more use of Bootstrap elements we'll add [reactstrap](https://reactstrap.github.io/) to our app. Also we want nice icons for our cart so we'll add [FontAwesome](https://github.com/FortAwesome/react-fontawesome) too.
25 |
26 | ```
27 | yarn add reactstrap
28 | yarn add @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome
29 | ```
30 |
31 | Finally add a little CSS to the end of our `index.css` file that will be used to space out text and icons within buttons.
32 |
33 | *src/index.css*
34 | ```
35 | .icon-button-text-right {
36 | padding-left: 0.5rem;
37 | }
38 | ```
39 |
40 | ## Using the Commerce.js cart object
41 |
42 | The Commerce.js API provides a number of endpoints for interacting with [carts](https://commercejs.com/docs/api/#carts) and these are encapsulated in the SDK's `cart` object.
43 |
44 | We'll need to interact with the cart in a number of places within our app:
45 |
46 | * In the `Cart` component to display the products that are in the cart
47 | * In the products list to be able to add products to the cart
48 |
49 | And there are likely to be more places in future as our application grows. Therefore we will manage the state of the cart in our top level `App` component and make it available to any child components that need it.
50 |
51 | In the standard React unidirectional data model state is passed *down* to child components through `props`. In addition, the parent component may also pass down event handler functions to update that state. This works well for small components with few children but tends to break down in situations like this where we want to access the cart in multiple children across our component hierarchy. Using the standard method we would have to pass the cart state and event handlers through multiple intermediate components that don't need to be concerned with the cart state at all.
52 |
53 | To get around this we'll be using React [Contexts](https://reactjs.org/docs/context.html). A Context allows state that is managed in a high level component to be published via a context *provider*. Then, any child components that need to use that state can declare a *consumer* of the context in order to access the state.
54 |
55 | We'll look at how we set this up for the cart in out `App` component and then build out the other components that will consume the context.
56 |
57 | ### Set up shared components
58 |
59 | We will need to access the context that we create in various places across our app. In addition we will also now need to use the Commerce.js SDK in more than one component so we'll put the initialisaton of these objects into separate files that we can include where necessary. First extract the Commerce.js initialisation by creating the file `src/lib/Commerce.js` with the following contents:
60 |
61 | ```
62 | import Commerce from '@chec/commerce.js';
63 |
64 | export const commerce = new Commerce(process.env.REACT_APP_CJS_PUBLICKEY_TEST);
65 | ```
66 |
67 | Notice that we're now using an *environment variable* (`REACT_APP_CJS_PUBLICKEY_TEST`) to store our API key rather than hard coding it. This is better for security. Check your operating system documentation for how to set environment variables.
68 |
69 | Now set up a file to initialise our context. Create `src/context/CartContext.js` with these lines.
70 |
71 | ```
72 | import React from 'react';
73 |
74 | const CartContext = React.createContext({
75 | cart: {},
76 | setCart: () => {}
77 | });
78 | export default CartContext;
79 | ```
80 |
81 | Here we're creating a Context for our cart with default values of an empty cart and an empty function for updating a cart. we'll replace these defaults when we initialise the context's provider.
82 |
83 | ### Use the context in the `App` component
84 |
85 | Modify the `App` component to retrieve the cart and make it available to its children using the context provider. Update `App.js` to the code below.
86 |
87 | *src/App.js*
88 | ```
89 | import React, { useEffect, useState } from 'react';
90 | import './App.css';
91 | import Nav from './components/Nav';
92 | import ProductList from './components/ProductList';
93 | import CartContext from './context/CartContext';
94 | import { commerce } from './lib/Commerce';
95 |
96 | function App() {
97 | const [cart, setCart] = useState();
98 |
99 | useEffect(() => {
100 | commerce.cart.retrieve()
101 | .then(cart => {
102 | setCart(cart);
103 | })
104 | }, [])
105 |
106 | return (
107 |
108 |
109 |
110 |
111 |
Products
112 |
113 |
114 |
115 |
116 | );
117 | }
118 |
119 | export default App;
120 | ```
121 |
122 | The `Nav` component is new and we'll create that shortly. It will include a button to toggle a modal dialogue that contains the cart itself. First though, let's look at how we're handling the cart state.
123 |
124 | We begin by importing the cart context and Commerce.js SDK from the files we created earlier. Next we use React's `useState` function to create the `cart` state and a function for setting that state.
125 |
126 | ```
127 | const [cart, setCart] = useState();
128 | ```
129 |
130 | Next we retrieve a cart object using the SDK and set it to the `cart` state. We've put this into a `useEffect` call. This can be thought of as the equivalent of `componentDidMount` which we saw in `ProductList` except for use in *functional* components, so this will be called when the component is rendered.
131 |
132 | ```
133 | useEffect(() => {
134 | commerce.cart.retrieve()
135 | .then(cart => {
136 | setCart(cart);
137 | })
138 | }, [])
139 | ```
140 |
141 | Here we're calling the `cart.retrieve` function of Commerce.js. Like most of the SDK functions it returns a promise that we're handling in `then` to set the returned cart object to the component's state.
142 |
143 | Finally we're creating a context provider to make the cart state and its associated update method available to the rest of the app.
144 |
145 | ```
146 |
147 |
148 |
149 |
Products
150 |
151 |
152 |
153 | ```
154 |
155 | Whenever we create a provider we need to give it a `value`, which is the object that will be accessible by the consumers of this context. Here we're providing the object `{cart, setCart}` which contains the cart state and setter function. This is a shorthand way of writing:
156 |
157 | ```
158 | {
159 | cart: cart,
160 | setCart: setCart
161 | }
162 | ```
163 |
164 | Notice that the provider component is wrapping both the `Nav` and `ProductList` components. This is necessary for them to be able to access the data from the context.
165 |
166 | Next we'll use the context to display the contents of the cart.
167 |
168 | ## Display the cart
169 |
170 | As mentioned above, the cart will be displayed in a modal dialog that is contained inside the `Nav` component. The component hierarchy will also include a wrapper for the entire cart, a list of cart products, a component for each line item in the cart and rows for the cart total and checkout button.
171 |
172 | The cart with products in will look something like the image below.
173 |
174 | 
175 |
176 | Let's build that hierarchy starting with the nav bar.
177 |
178 | *src/components/Nav.js*
179 | ```
180 | import React, { useState, useContext } from 'react';
181 | import {
182 | Navbar,
183 | NavbarBrand,
184 | Button,
185 | Modal,
186 | ModalHeader,
187 | ModalBody
188 | } from 'reactstrap';
189 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
190 | import { faCartArrowDown } from '@fortawesome/free-solid-svg-icons'
191 | import Cart from './cart/Cart';
192 | import CartContext from '../context/CartContext';
193 |
194 | const Nav = () => {
195 | const { cart } = useContext(CartContext);
196 | const [modal, setModal] = useState(false);
197 | const toggleModal = () => setModal(!modal);
198 |
199 | let cartItems = cart && cart.total_unique_items > 0 ? cart.total_unique_items : '';
200 | return (
201 |
202 |
203 |
MyStore
204 |
205 |
209 |
210 | Cart
211 |
212 |
213 |
214 |
215 |
216 | )
217 | }
218 |
219 | export default Nav;
220 | ```
221 |
222 | Much of this component consists of `reactstrap` components for creating the modal dialog and toggling its visibility. We are however accessing the `cart` state so that the cart button can include the number of items that are in the cart. We consume the `cart` state from the context with the `useContext` function. We're just using `cart` here, we won't need `setCart` because we aren't modifying the state.
223 |
224 | ```
225 | const { cart } = useContext(CartContext);
226 | ```
227 |
228 | We then use the `total_unique_items` attribute of the cart to display the number of items in the cart as text on the button.
229 |
230 | Next we'll create the cart component itself in the file `src/components/cart/Cart.js`.
231 |
232 | ```
233 | import React, { useContext } from 'react';
234 | import CartProductList from './CartProductList';
235 | import CartCheckoutRow from './CartCheckoutRow';
236 | import CartTotalRow from './CartTotalRow';
237 | import CartContext from '../../context/CartContext';
238 |
239 | const Cart = () => {
240 | const { cart } = useContext(CartContext);
241 |
242 | if (cart && cart.total_unique_items > 0) {
243 | return (
244 |
245 |
246 |
247 |
248 |
249 | );
250 | }
251 |
252 | return (
253 |
254 |
Your cart is currently empty.
255 |
256 | )
257 | }
258 |
259 | export default Cart;
260 |
261 | ```
262 |
263 | This again consumes the cart context and passes the `cart` object to the product list and total row as a prop. If there are no line items yet it will render a message that the cart is empty.
264 |
265 | The cart product list will loop over the line items from the cart and render a row for each.
266 |
267 | *src/components/cart/CartProductList.js*
268 | ```
269 | import React from 'react';
270 | import CartProductRow from './CartProductRow';
271 |
272 | const CartProductList = ({ cart }) => {
273 | return (
274 | cart.line_items.map(line_item => {
275 | return
276 | })
277 | );
278 | }
279 |
280 | export default CartProductList;
281 | ```
282 |
283 | We access the items in the cart using its `line_items` property and `map` this to a list of `CartProductRow` components. Here is that component.
284 |
285 | *src/components/cart/CartProductRow.js*
286 | ```
287 | import React from 'react';
288 |
289 | const CartProductRow = ({ lineItem }) => {
290 | return (
291 |
305 | );
306 | }
307 |
308 | export default CartProductRow;
309 | ```
310 |
311 | The `line_item` has many properties that are similar to a standard product with the addition of a `quantity` and `line_total`, which is the price of the product multiplied by the quantity.
312 |
313 | We also have components to display the current cart total and a checkout button.
314 |
315 | *src/components/cart/CartTotalRow.js*
316 | ```
317 | import React from 'react';
318 |
319 | const CartTotalRow = ({ cart }) => {
320 | return (
321 |
400 | );
401 | }
402 | }
403 |
404 | ProductList.contextType = CartContext;
405 |
406 | export default ProductList;
407 | ```
408 |
409 | There are some notable changes here unrelated to the context. We've replaced the original code that initialised the Commerce.js SDK with an import from our new initialiser. We're also passing the `product` object itself to the `ProductRow` as a single prop rather than splitting it out into individual props for name, image etc. This is because we will now need to access the ID of the product in the row and the number of props was becoming unwieldy.
410 |
411 | The way that contexts are consumed in class components is slightly different from functional components. After defining the class we must set its `contextType`.
412 |
413 | ```
414 | ProductList.contextType = CartContext;
415 | ```
416 |
417 | This makes the cart context available within the component as `this.context`.
418 |
419 | We've also defined a function to handle the addition of an individual product to the cart. This will be passed the ID of the product to add.
420 |
421 | ```
422 | handleAddProduct(productId) {
423 | commerce.cart.add(productId, 1)
424 | .then(result => {
425 | this.context.setCart(result.cart);
426 | alert("Product added to cart");
427 | });
428 | }
429 | ```
430 |
431 | We also need to bind this function to `this` in our object's constructor so that we can access `this.context`.
432 |
433 | ```
434 | this.handleAddProduct = this.handleAddProduct.bind(this);
435 | ```
436 |
437 | The handler function calls `commerce.cart.add` with the product ID and a quantity. Since this is a simple 'add to cart' button the quantity will always be 1. The function returns a promise whose result contains an updated `cart` object, which we pass to the `setCart` function in the `CartContext`. This will update the `cart` state way up the hierarchy in the `App` component. React will notice this change and re-render all components that use the cart.
438 |
439 | How does our `handleAddProduct` function get called? We'll pass it as a prop to each row in our products loop and use it in the `onClick` method of the "Add to cart" button.
440 |
441 | ```
442 | this.state.products.map(product => {
443 | return
444 | })
445 | ```
446 |
447 | *src/components/ProductRow.js*
448 | ```
449 | import React from 'react';
450 | import { Button } from 'reactstrap';
451 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
452 | import { faPlus } from '@fortawesome/free-solid-svg-icons'
453 |
454 | const ProductRow = ({ product, addProduct }) => {
455 | const handleAddProduct = e => {
456 | e.preventDefault();
457 | addProduct(product.id);
458 | }
459 |
460 | return (
461 |
462 |
463 |
464 |
465 |
466 |
{product.name}
467 |
468 |
469 |
470 |
{product.price.formatted_with_symbol}
471 |
475 |
476 |
477 | );
478 | }
479 |
480 | export default ProductRow;
481 | ```
482 |
483 | In the `ProductRow` component we've created an event handler that will call the `addProduct` function that was passed as a prop. It will pass the ID of the product that this row represents.
484 |
485 | ```
486 | const handleAddProduct = e => {
487 | e.preventDefault();
488 | addProduct(product.id);
489 | }
490 | ```
491 |
492 | We've also added an "Add to cart" button with this event handler as its `onClick` prop. It is also using the FontAwesome *plus* icon.
493 |
494 | ```
495 |
499 | ```
500 |
501 | Now, when you click the Add to cart button you the number in the cart button should increase.
502 |
503 | 
504 |
505 | And when you click it the cart modal should contain the items you've added.
506 |
507 | ## Limitations
508 |
509 | In this guide we've only covered adding products to cart. There is much more we'd need to do to make the cart functional, such as editing item quantities, removing items entirely and clearing the cart. Also, when we try to add the same product multiple times the quantity remains at 1, ideally we'd like this action to update the quantity too. The techniques used in this guide can be applied to all of these actions to make the cart more functional.
510 |
511 | ## Adding "Recommended Products"
512 |
513 | A common feature of shopping carts is a "recommended products" section which is designed to show shoppers other items that they may like in order to encourage further purchases. We're going to create a very simple version of this that will simply display a product that isn't already in the cart. In reality you would want this to be more sophisticated, for example by using [machine learning](https://cloud.google.com/solutions/machine-learning/recommendation-system-tensorflow-overview).
514 |
515 | Create a `RecommendedProduct` component in `src/components/cart/RecommendedProduct.js` with this content.
516 |
517 | ```
518 | import React, { useState, useEffect } from 'react';
519 | import { commerce } from '../../lib/Commerce';
520 |
521 | const RecommendedProduct = ({ cart }) => {
522 | const [recommendedProduct, setRecommendedProduct] = useState(false);
523 |
524 | const cartProductIds = cart.line_items.map(line_item => line_item.product_id);
525 |
526 | useEffect(() => {
527 | commerce.products.list().then((result) => {
528 | result.data.some((product) => {
529 | if (!cartProductIds.includes(product.id)) {
530 | setRecommendedProduct(product);
531 | return true;
532 | }
533 | });
534 | });
535 | }, [])
536 |
537 | if (recommendedProduct) {
538 | return (
539 | <>
540 |
556 | >
557 | );
558 | }
559 |
560 | return (
561 | <>
562 | >
563 | );
564 | }
565 |
566 | export default RecommendedProduct;
567 | ```
568 |
569 | Let's go through what this is doing.
570 |
571 | First it is setting a recommended product to state so that the component will re-render when it changes. The default value is `false`.
572 |
573 | ```
574 | const [recommendedProduct, setRecommendedProduct] = useState(false);
575 | ```
576 |
577 | Next it is creating a list of the IDs of products that are already in the cart.
578 |
579 | ```
580 | const cartProductIds = cart.line_items.map(line_item => line_item.product_id);
581 | ```
582 |
583 | Then it is retrieving the list of products and searching it for a product that isn't in the cart so that we don't recommend something that the shopper has already added. This is happening in a `useEffect` call so that it refreshes whenever the component is rendered, e.g. when the cart changes. Note the use of the `some` method on the array of products in `result.data`. `some` is useful in this case because it ends the loop the first time the function it is passed returns `true`, so we stop looping as soon as we find a product that isn't in the cart.
584 |
585 | ```
586 | useEffect(() => {
587 | commerce.products.list().then((result) => {
588 | setRecommendedProduct(false);
589 | result.data.some((product) => {
590 | if (!cartProductIds.includes(product.id)) {
591 | setRecommendedProduct(product);
592 | return true;
593 | }
594 | });
595 | });
596 | }, [])
597 | ```
598 |
599 | Finally, if a recommended product exists it will display the product details.
600 |
601 | The recommendation should look something like the image below.
602 |
603 | 
604 |
605 | ### Add the recommended product to the cart
606 |
607 | Seeing a recommended product is great but it would be even better to be able to add it to the cart directly. We'll implement an Add To Cart button for this component that will look something like this:
608 |
609 | 
610 |
611 | Our add to cart functionality currently resides on the `ProductList` component and isn't accessible from the cart because it is in a different part of the component hierarchy. To address this we'll promote the functionality to the `App` component and add it to our `CartContext`.
612 |
613 | First add an empty function to the default cart context.
614 |
615 | *src/context/CartContext.js*
616 | ```
617 | const CartContext = React.createContext({
618 | cart: {},
619 | setCart: () => {},
620 | addProductToCart: () => {},
621 | });
622 | ```
623 |
624 | Then implement the function in the `App` component, just after the existing `useState` call. The body of this function will be the same as the existing function in the product list.
625 |
626 | *src/App.js*
627 | ```
628 | const [cart, setCart] = useState();
629 |
630 | const addProductToCart = (productId) => {
631 | commerce.cart.add(productId, 1)
632 | .then(result => {
633 | setCart(result.cart);
634 | alert("Product added to cart");
635 | });
636 | }
637 | ```
638 |
639 | Pass this down to other components in the context provider.
640 |
641 | *src/App.js*
642 | ```
643 |
644 |
645 |
646 |
Products
647 |
648 |
649 |
650 | ```
651 |
652 | In our existing `ProductList` we can now remove the old fuction and call this new one from the context.
653 |
654 | *src/components/ProductList.js*
655 | ```
656 | handleAddProduct(productId) {
657 | this.context.addProductToCart(productId);
658 | }
659 | ```
660 |
661 | Adding products to the cart from the main product list should now be working as before so we can move on to implementing it for the recommended product.
662 |
663 | In the `Cart` component, reference the new function from the context and pass it down to the `RecommendedProduct` as a prop, alongside the cart object.
664 |
665 | *src/components/cart/Cart.js*
666 | ```
667 | const { cart, addProductToCart } = useContext(CartContext);
668 |
669 | if (cart && cart.total_unique_items > 0) {
670 | return (
671 |
672 |
673 |
674 |
675 |
676 |
677 | );
678 | }
679 | ```
680 |
681 | Now import the button component from reactstrap in `RecommendedProduct`.
682 |
683 | *src/components/cart/RecommendedProduct.js*
684 | ```
685 | import { Button } from 'reactstrap';
686 | ```
687 |
688 | And add a button and handler function when there is a recommended product available.
689 |
690 | *src/components/cart/RecommendedProduct.js*
691 | ```
692 | if (recommendedProduct) {
693 | const handleAddProduct = e => {
694 | e.preventDefault();
695 | addProductToCart(recommendedProduct.id);
696 | setRecommendedProduct(null)
697 | }
698 |
699 | return (
700 | <>
701 |