DEV Community

Cover image for NextJS simple shopping cart
Ha Tuan Em
Ha Tuan Em

Posted on • Updated on

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

Discussion (13)

Collapse
justicebringer profile image
Gabriel

It was good if you showed the all code of the index.js file.
For other guys who were stuck on the creation of the store, follow the Usage guide: github.com/kirill-konshin/next-red...

Long story short, in your _app.tsx file:

import React, {FC} from 'react';
import {AppProps} from 'next/app';
import {wrapper} from '../components/store';

const WrappedApp: FC<AppProps> = ({Component, pageProps}) => (
    <Component {...pageProps} />
);

export default wrapper.withRedux(WrappedApp);
Enter fullscreen mode Exit fullscreen mode
Collapse
hte305 profile image
Ha Tuan Em Author • Edited

Absolutely, I had edited this article with link repository below the article.
Thanks you for reading.
And I got have a new post deploy express to Vercel. Hope you will read this article.

Collapse
michelc profile image
Michel

But this is not an Express application? Are you planning a post to explain how you deployed your shopping cart to Vercel?

Thread Thread
hte305 profile image
Ha Tuan Em Author

Yep, if you want, I will do that for you. Follow me, it's comming in this week.

Collapse
justicebringer profile image
Gabriel

An upgraded setCookie function

export function setCookie(
  key: string,
  value: string | object,
  days: number = 30,
  sameSite: 'Lax' | 'strict' | 'Strict' | 'lax' | 'none' | 'None' | undefined = 'Lax'
) {
  let expires: number | Date | undefined;
  if (days) {
    let date = new Date();
    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
    expires = date;
  }

  jsCookie.set(key, JSON.stringify(value), { expires: expires, sameSite: sameSite });
}
Enter fullscreen mode Exit fullscreen mode
Collapse
hte305 profile image
Ha Tuan Em Author

Thanks you for code.

Collapse
uguremirmustafa profile image
uguremirmustafa

Thank you, setCookie and getCookie was halpful for me!

Collapse
hte305 profile image
Ha Tuan Em Author

You are welcome πŸ˜€πŸ˜‰

Collapse
kleguizamon profile image
Kevin Leguizamon

GreatπŸ‘ŒπŸ½

Collapse
hte305 profile image
Ha Tuan Em Author

Thanks πŸ˜ƒ

Collapse
orionitcenter profile image
Orion IT Center

Hi Good day
How to display the cart page?
thank you.

Collapse
hte305 profile image
Ha Tuan Em Author

Everything has saved in cookie. You can use getCookie function to fetch data and processing with your logic.

Collapse
couponsvacom profile image
Couponsva

Thanks you for code.
And I got have a new post couponsva. Hope you will read this article.