NextJS simple shopping cart
Tags: node, nextjs, redux, react
Published: 2021-03-14 12:45:13

After a week of learning and working with Next.JS. I had to build a simple application using the topic is a shopping cart in e-commerce. A lot of different knowledge in the framework when I deep down to learn, because I tried to compare MERN and NEXT.JS. I knew it was wrong but I did. Anyone will do like that - bring the old things to a new house. Something is beautiful and something is stuff.

And something stuff which I got in this framework. One of them that is `global windows variable is not ready in all the time` - that means the client-side and server-side are in black and white.

That's why I need some third party:
- `js-cookie` to manage the resource in client-side.
- `next-redux-wrapper` to manage the state in the client-side.
- `redux` and `etc...`


## First of all
I need create the next application and add third party to project
```bash
create-next-app next-simple-shopping && cd next-simple-shopping

yarn add js-cookie next-redux-wrapper react-redux redux redux-devtools-extension redux-thunk
```

## 🍪 Setup the cookie to application 

```js
// ./libs/useCookie.js
import jsCookie from "js-cookie";

export function getCookie(key) {
  let result = [];
  if (key) {
    const localData = jsCookie.get(key);
    if (localData && localData.length > 0) {
      result = JSON.parse(localData);
    }
  }

  return result;
}

export function setCookie(key, value) {
  jsCookie.set(key, JSON.stringify(value));
}

// cookie ready to serve
```
## 🏡 Setup the redux to make the magic in the client side

### Initial the store component in redux

```js
// ./store/index.js
import { createStore, applyMiddleware, combineReducers } from "redux";
import { HYDRATE, createWrapper } from "next-redux-wrapper";
import thunkMiddleware from "redux-thunk";
import shopping from "./shopping/reducer";

const bindMiddleware = (middleware) => {
  if (process.env.NODE_ENV !== "production") {
    const { composeWithDevTools } = require("redux-devtools-extension");
    return composeWithDevTools(applyMiddleware(...middleware));
  }
  return applyMiddleware(...middleware);
};

const combinedReducer = combineReducers({
  shopping,
});

const reducer = (state, action) => {
  if (action.type === HYDRATE) {
    const nextState = {
      ...state, // use previous state
      ...action.payload, // apply delta from hydration
    };
    return nextState;
  } else {
    return combinedReducer(state, action);
  }
};

const initStore = () => {
  return createStore(reducer, bindMiddleware([thunkMiddleware]));
};

export const wrapper = createWrapper(initStore);
```
### Also, we need the `action` and `reducer` in the application, too.

#### Action of shopping cart
```js
// ./libs/shopping/action.js
export const actionShopping = {
  ADD: "ADD",
  CLEAR: "CLEAR",
  FETCH: "FETCH",
};

export const addShopping = (product) => (dispatch) => {
  return dispatch({
    type: actionShopping.ADD,
    payload: {
      product: product,
      quantity: 1,
    },
  });
};

export const fetchShopping = () => (dispatch) => {
  return dispatch({
    type: actionShopping.FETCH,
  });
};

export const clearShopping = () => (dispatch) => {
  return dispatch({
    type: actionShopping.CLEAR,
  });
};
```

#### Reducer of shopping cart
```js
// ./libs/shopping/reducer.js
import { getCookie, setCookie } from "../../libs/useCookie";
import { actionShopping } from "./action";
const CARD = "CARD";

const shopInitialState = {
  shopping: getCookie(CARD),
};

function clear() {
  let shoppings = [];
  setCookie(CARD, shoppings);
  return shoppings;
}

function removeShoppingCart(data) {
  let shoppings = shopInitialState.shopping;
  shoppings.filter((item) => item.product.id !== data.product.id);
  setCookie(CARD, shoppings);
  return shoppings;
}

function increment(data) {
  let shoppings = shopInitialState.shopping;
  let isExisted = shoppings.some((item) => item.product.id === data.product.id);
  if (isExisted) {
    shoppings.forEach((item) => {
      if (item.product.id === data.product.id) {
        item.quantity += 1;
      }
      return item;
    });
  }
  setCookie(CARD, shoppings);
  return shoppings;
}

function decrement(data) {
  let shoppings = shopInitialState.shopping;
  let isExisted = shoppings.some((item) => item.product.id === data.product.id);
  if (isExisted) {
    shoppings.forEach((item) => {
      if (item.product.id === data.product.id) {
        item.quantity -= 1;
      }
      return item;
    });
  }
  setCookie(CARD, shoppings);
  return shoppings;
}

function getShopping() {
  return getCookie(CARD);
}

function addShoppingCart(data) {
  let shoppings = shopInitialState.shopping;
  let isExisted = shoppings.some((item) => item.product.id === data.product.id);
  if (isExisted) {
    shoppings.forEach((item) => {
      if (item.product.id === data.product.id) {
        item.quantity += 1;
      }
      return item;
    });
  } else {
    shoppings.push(data);
  }
  setCookie(CARD, shoppings);
  return shoppings;
}

export default function reducer(state = shopInitialState, action) {
  const { type, payload } = action;

  switch (type) {
    case actionShopping.ADD:
      state = {
        shopping: addShoppingCart(payload),
      };
      return state;
    case actionShopping.CLEAR:
      state = {
        shopping: clear(),
      };
      return state;
    case actionShopping.FETCH:
    default:
      state = {
        shopping: getShopping(),
      };
      return state;
  }
}
```
### **Okay, the redux is ready to serve 🎂.**

## Making *`two`* **`component`** for easy manage the state in the client.

### > Product component 🩳
```js
// ./components/ProductItem.jsx
import React from "react";
import styles from "../styles/Home.module.css";
import Image from "next/image";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { addShopping } from "../store/shopping/action";

const ProductItem = (props) => {
  const {
    data: { id, name, price, image },
    addShopping,
  } = props;
  return (
    <div className={styles.card}>
      <Image src={image} alt={name} height="540" width="540" />
      <h3>{name}</h3>
      <p>{price}</p>
      <button onClick={() => addShopping(props.data)}>Add to card</button>
    </div>
  );
};

const mapDispatchTopProps = (dispatch) => {
  return {
    addShopping: bindActionCreators(addShopping, dispatch),
  };
};

export default connect(null, mapDispatchTopProps)(ProductItem);
```
### > Shopping counter component 🛒

```js
import React, { useEffect, useState } from "react";
import { fetchShopping, clearShopping } from "../store/shopping/action";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";

const ShoppingCounter = ({ shopping, fetchShopping, clear }) => {
  useEffect(() => {
    fetchShopping();
  }, []);

  return (
    <div
      style={{
        position: "relative",
        width: "100%",
        textAlign: "right",
        marginBottom: "1rem",
      }}
    >
      <h2
        style={{
          padding: "1rem 1.5rem",
          right: "5%",
          top: "5%",
          position: "absolute",
          backgroundColor: "blue",
          color: "white",
          fontWeight: 200,
          borderRadius: "10px",
        }}
      >
        Counter <strong>{shopping}</strong>
        <button
          style={{
            borderRadius: "10px",
            border: "none",
            color: "white",
            background: "orange",
            marginLeft: "1rem",
            padding: "0.6rem 0.8rem",
            outline: "none",
            cursor: "pointer",
          }}
          onClick={clear}
          type="button"
        >
          Clear
        </button>
      </h2>
    </div>
  );
};

const mapStateToProps = (state) => {
  const data = state.shopping.shopping;
  const count =
    data.length &&
    data
      .map((item) => item.quantity)
      .reduce((item, current) => {
        return item + current;
      });
  return {
    shopping: count,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    fetchShopping: bindActionCreators(fetchShopping, dispatch),
    clear: bindActionCreators(clearShopping, dispatch),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(ShoppingCounter);
```
## **Ops! Don't forget your data mock based on the path to index page**

```js
// ./pages/index.js
import { products } from "../mocks/data";
import ShoppingCounter from "../components/ShoppingCounter";
import ProductItem from "../components/ProductItem";
// ...
<ShoppingCounter />
<main className={styles.main}>
  <h1 className={styles.title}>Welcome to Next.js shopping 🩳!</h1>
  <div className={styles.grid}>
    {products &&
      products.map((product) => (
        <ProductItem key={product.id} data={product} />
      ))}
  </div>
</main>
//...
```
See the live demo [simple-shopping-cart](https://next-shopping-example-git-master-hatuanem.vercel.app)

Okay, let's try for yourself. That's is my dev note. Thanks for reading and see you in the next article.

Here is [repository](https://github.com/hatuanem199801/next-shopping-example)