The purpose of developing this SPA
app is practising Redux-middleware
.
I always use Function Components in web development, in this app I used Class Component
because I believe some developers are still using it so it is a good opportunity to do some practice.
Please visit this app in Mobile Mode
👉 https://main.d24qeurrp3ff0q.amplifyapp.com/
This is the home page of this app. I like the user interface as it is simple and easy to understand.
All the mock data are stored under the Public/mock
file which is easy to access.
Products
are stored in an Array
, and the structure of the data will be like that
[
{
"id": "p-100",
"shopIds": ["s-100"],
"shop": "Dancing club",
"tag": "noBooking",
"product": "DancingClub",
"currentPrice": 1,
"oldPrice": 400,
"picture": "https://p1.meituan.net/dpdeal/a8eb71748e1f4df175668368e98bb4f868511.jpg.webp@120w_90h_1e_1c_1l|watermark=1&&r=1&p=9&x=20&y=20"
},
{...},
{...}
]
Array allows me to map and re-structure the data easily. The new data stored in Object
will be like that
Object
allows me to use Object.keys()
or Object.values()
to get whatever I need. The data structure of shops
and orders
will be the same.
Axios
is definetly the best option but I insisted using Fetch
, because it is built into most modern browsers and no installation is required as such. Another reason is that I don't need XSRF protection and interceptor.
const headers = new Headers({
"Accept": "application/json",
"Content-Type": "application/json"
})
function get(url) {
return fetch(url, {
method: "GET",
headers: headers
}).then(response => {
return handleResponse(url, response);
}).catch(error => {
return Promise.reject({error: {message: "Request failed."}})
})
}
function post(url, data) {
return fetch(url, {
method: "POST",
headers: headers,
body: data
}).then(response => {
return handleResponse(url, response);
}).catch(error => {
return Promise.reject({error: {message: "Request failed."}})
})
}
What I need to just make sure is that it provide and accept entities as JSON
.
useContext
+ useReducer
is becoming more and more popular but I stick with React-redux
. It provides many API can make the code more readable and easy to understand. Connet()
can easily connect the React component to Redux which is awesome for this application.
The data can divided into to two parts. The first part is entites
, which stored product details, shope details, user orders and search history. The second part is the state based on each pages like home, producet detils, login, user information, purchase and so on.
entities
includes multiple state, and each state has its own schema
which describes the shape of the state.
After receiving response, the response data will be normalized
and return a new form
const normalizeData = (data, schema) => {
const {id, name} = schema
let kvObj = {}
let ids = []
if(Array.isArray(data)) {
data.forEach(item => {
kvObj[item[id]] = item
ids.push(item[id])
})
} else {
kvObj[data[id]] = data
ids.push(data[id])
}
return {
[name]: kvObj,
ids
}
}
[name]: kvObj
will be the state for [name] entity
(for example 'shop') while ids
array will be the state for different pages like home
page.
The state
stored in entity
The state
stored in pages, for example home pages
. In this page, I just need to know which item is under discount or user will like. Therefore, an array of id
is the best way to let the app know which item should go which section.
Beside entities
, the rest of state will be divided to multiple parts according to the pages.
home - item id array
detail - selected item detail
login - if user is login the app user information
user - user's order
The reason I used redux-middleware
is DRY Priciple
. In this app, same logic will go again and again and I am tired of writing the same code. Another important thing is it is hard to maintain so I decieded to use middleware
.
The action is named in [FETCH_DATA]
. endpoint
will be the url
while the schema
describes the shape of the data will be.
{
[FETCH_DATA]: {
types: [
types.FETCH_LIKES_REQUEST,
types.FETCH_LIKES_SUCCESS,
types.FETCH_LIKES_FAILURE
],
endpoint,
schema
}
}
This middleware will be called when an action comes with [FETCH_DATA]
const callAPI = action[FETCH_DATA]
if(typeof callAPI === 'undefined') {
return next(action)
}
const { endpoint, schema, types } = callAPI
if(typeof endpoint !== 'string') {
throw new Error('endpoint must be a string type URL')
}
if(!schema) {
throw new Error('undefined schema')
}
if(!Array.isArray(types) && types.length !== 3) {
throw new Error('must be an array with 3 action types')
}
if(!types.every(type => typeof type === 'string')) {
throw new Error('action type must be string type')
}
const actionWith = data => {
const finalAction = {...action, ...data}
delete finalAction[FETCH_DATA]
return finalAction
}
const [requestType, successType, failureType] = types
next(actionWith({type: requestType}))
return fetchData(endpoint, schema).then(
response => next(actionWith({
type: successType,
response
})),
error => next(actionWith({
type: failureType,
error: error.message || 'data fetching failed'
}))
)
when user scroll to the bottom, it will immediately fetch new data.
The most important reason I am using AWS Amplify
is it's easy to use. The Amplify Console provides a continuous delivery and hosting service for web applications, which means front-end developer doesn't need to know much about web app deployment while AWS Amplify
will do everything for you.