Ha Tuan Em
Posted on March 14, 2021
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
andetc...
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
🍪 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
🏡 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);
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,
});
};
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;
}
}
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);
> 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);
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>
//...
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
Posted on March 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.