NextJS simple shopping cart

hte305

Ha Tuan Em

Posted on March 14, 2021

NextJS simple shopping cart

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

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
Enter fullscreen mode Exit fullscreen mode

🍪 Setup the cookie to application

// ./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
Enter fullscreen mode Exit fullscreen mode

🏡 Setup the redux to make the magic in the client side

Initial the store component in redux

// ./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);
Enter fullscreen mode Exit fullscreen mode

Also, we need the action and reducer in the application, too.

Action of shopping cart

// ./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,
  });
};
Enter fullscreen mode Exit fullscreen mode

Reducer of shopping cart

// ./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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Okay, the redux is ready to serve 🎂.

Making two component for easy manage the state in the client.

> Product component 🩳

// ./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);
Enter fullscreen mode Exit fullscreen mode

> Shopping counter component 🛒

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);
Enter fullscreen mode Exit fullscreen mode

Ops! Don't forget your data mock based on the path to index page

// ./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>
//...
Enter fullscreen mode Exit fullscreen mode

See the live demo simple-shopping-cart

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

💖 💪 🙅 🚩
hte305
Ha Tuan Em

Posted on March 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

NextJS simple shopping cart
node NextJS simple shopping cart

March 14, 2021