HTML CSS Javascript
workshop2 slides
Resource:
- List and Keys
- form, input elements
- W3School
- useSelector and useDispatch with Redux
- add/delete counter example
- another example
- Start the assignment!
- react add lists
- redux awesome tutorial!
- Pop up awesome tutorial
- To do list for advanced project
In this assignemnt, I used toolkit to create reducer instead of using tradiationl reducer.
import { createSlice } from '@reduxjs/toolkit';
import { REQUEST_STATE } from "../utils";
import { getRecipesAsync, addRecipeAsync, deleteRecipeAsync, updateRecipeAsync } from './thunks';
const INITIAL_STATE = {
recipeList: [],
getRecipes: REQUEST_STATE.IDLE,
addRecipe: REQUEST_STATE.IDLE,
deleteRecipe: REQUEST_STATE.IDLE,
updateRecipe: REQUEST_STATE.IDLE,
error: null
};
const recipesSlice = createSlice({
name: 'recipes',
initialState: INITIAL_STATE,
reducer: {},
extraReducers: (builder) => {
builder
.addCase(getRecipesAsync.pending, (state) => {
state.getRecipes = REQUEST_STATE.PENDING;
state.error = null;
})
.addCase(getRecipesAsync.fulfilled, (state, action) => {
state.getRecipes = REQUEST_STATE.FULFILLED;
state.recipeList = action.payload;
})
.addCase(getRecipesAsync.rejected, (state, action) => {
state.getRecipes = REQUEST_STATE.REJECTED;
state.error = action.error;
})
}
});
export default recipesSlice.reducer;
And then create thunks to call fetch REST APIs
import { createAsyncThunk } from "@reduxjs/toolkit";
import { actionTypes } from "./actionTypes";
export const getRecipesAsync = createAsyncThunk(
actionTypes.GET_RECIPES,
async () => {
return await RecipeService.getRecipes();
}
);
const getRecipes = async () => {
const response = await fetch('http://localhost:3001/recipes', {
method: 'GET'
});
// console.log(response.json());
return response.json();
};
const addRecipe = async (recipe) => {
console.log(recipe);
const response = await fetch('http://localhost:3001/recipes', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(recipe)
});
const data = await response.json();
console.log(data);
if (!response.ok) {
const errorMsg = data?.message;
throw new Error(errorMsg)
}
return data;
};
Once the page was been loaded, I will dispatch the "action" in the react component.
useEffect(() => {
dispatch(getRecipesAsync());
}, [dispatch]);
In the server side, I use "express" and routes. Example:
const express = require('express');
const router = express.Router();
router.get('/', function (req, res, next) {
return res.send(recipeList);
});
// create recipe
router.post('/', function (req, res, next) {
if (!req.body.name) {
return res.status(400).send({ message: 'Recipe must have a name!' })
} else if (!req.body.ingredients) {
return res.status(400).send({ message: 'Recipe must have ingredients!' })
} else if (!req.body.steps) {
return res.status(400).send({ message: 'Recipe must have steps!' })
}
const recipe = { id: uuid(), name: req.body.name, ingredients: req.body.ingredients, steps: req.body.steps };
recipeList.push(recipe);
console.log(recipe);
return res.send(recipe);
});
In summary, the main page dispatch action when redered, the action call fetch REST APIs which I implemented in server side. Then the action will return result which is res.send(recipe) to reducer. The reducer will change the state in store in redux.
Note: The response body of such a request should be the result of that operation - the entities that have been successfully created. To return the whole list is simply not standard. Imagine you have thousands of recipes. If you POST one recipe, do you want to get the whole list back? This requires extra memory resources and potentially computational resources to handle in your server. If you can't return the whole list because the response body would be bigger than what you can handle (ex AWS ELB has a 1MB response size limit), how do you decide what to return?
Now client side, computations within the app aren't as expensive. The code is all there and manipulating what you have probably isn't too difficult. But making an API call can be expensive. What if your app is primarily used on mobile? Will your users be happy if you're eating all their mobile data making a bunch of API requests? Probably not. It'll probably also take longer to return a whole list with your API call than returning just the new entities.
node.js
- workshop3 slides
- How to build a REST API with Node js & Express
- node.js tutorial
- Express JS Crash Course
- Express tutorial part 2
- express and js 35 minute
- redux and express
- redux, middleware and thunks
- redux toolkit(used this in asm)
some important mongoshell: show databases
[primary] recipeApp> show dbs
recipeApp 56.00 KiB
admin 372.00 KiB
local 1.24 GiB
find or show documents
[primary] recipeApp> db.recipes.find()
[
{
_id: ObjectId("62bcc59bc5f03b095d2fca41"),
id: '0',
name: 'sushi',
ingredients: 'meat, rice, shrimp',
steps: 'take rice, stack the meat, stack the shrimp'
},
{
_id: ObjectId("62bcc59bc5f03b095d2fca42"),
id: '1',
name: 'steak',
ingredients: 'beef',
steps: 'stack the meat'
}
]
insert or create a document
[primary] recipeApp> db.recipes.insert({name: "demo"})
{
acknowledged: true,
insertedIds: { '0': ObjectId("62bceed9c5f03b095d2fca44") }
}
update documents using $set
[primary] recipeApp> db.recipes.update({id: "1"},
... {
..... $set: {
....... like: 0,
....... date: Date()
....... }
..... }
... )
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
update documents using i$nc
[primary] recipeApp> db.recipes.update({id: "0"}, { $inc: {like: 2}})
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
update all fields using $rename
[primary] recipeApp> db.recipes.updateMany({},{ $rename: {like: 'likes'}})
{
acknowledged: true,
insertedId: null,
matchedCount: 2,
modifiedCount: 2,
upsertedCount: 0
}
delete or remove documents
[primary] recipeApp> db.recipes.remove({name: "demo"})
{ acknowledged: true, deletedCount: 1 }
After being familiar with MongoDB using shell. Let's see how we connect our server using mongoose!
First be sure you have MongoDB and Node.js installed.
next install Mongoose form the command line using npm
$ npm install mongoose --save
- In server side, create a config folder, create a file db.js for set up environment for connecting MongoDB using mongoose
const mongoose = require('mongoose');
const connectDB = async () => {
try {
// the link inside connect() can be found in MongoDb connection with application
const conn = await mongoose.connect('mongodb+srv://recipe:recipe@recipecluster.dx8jp.mongodb.net/recipeApp?retryWrites=true&w=majority');
console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline);
} catch (error) {
console.log(error);
process.exit(1);
}
}
module.exports = connectDB;
In the app.js, we need to connect database
const colors = require('colors');
//initialize DB connection
const connectDB = require('./config/db');
connectDB();
- Create Recipe Model. creating recipeModel.js file in models folder.
With Mongoose, everything is derived from a Schema. So far we have got a recipeSchema with 5 property. The next step is compiling our schema into a Model.
A model is a class with which we construct documents.
const mongoose = require('mongoose');
// create schema
const recipeSchema = mongoose.Schema(
{
name: {
type: String,
required: [true, 'Please add a name value'],
},
ingredients: {
type: String,
required: [true, 'Please add a ingredients value'],
},
steps: {
type: String,
required: [true, 'Please add a steps value'],
},
likes: {
type: Number
},
date: {
type: Date
},
},
{
timestamps: true,
}
);
// NOTE: methods must be added to the schema before compiling it with mongoose.model()
recipeSchema.methods.speak = function speak() {
console.log(`I am a recipe named ${this.name}`);
}
// create model
// The first argument is the singular name of the collection your model is for
const Recipe = mongoose.model('Recipe', recipeSchema);
module.exports = Recipe;
- In router/recipes.js , we don't need the hard code intial state anymore because we use the data from database.
We will use some mongoose API to fetch, create, update or delete data just like we did in Mongo shell above.
const asyncHandler = require('express-async-handler');
const Recipe = require('../models/recipeModel');
get one specific recipe using id
router.get('/:recipeId',asyncHandler(async function (req, res, next) {
Recipe.findById(req.params.recipeId, (err, recipe) => {
if (err) {
// console.log(err);
return res.status(404).send({ message: 'recipe not found' });
} else {
console.log(recipe);
return res.json({msg: "get recipe", recipe});
}
});
}));
create recipe
router.post('/',asyncHandler(async function (req, res, next) {
if (!req.body.name) {
return res.status(400).send({ message: 'Recipe must have a name!' })
} else if (!req.body.ingredients) {
return res.status(400).send({ message: 'Recipe must have ingredients!' })
} else if (!req.body.steps) {
return res.status(400).send({ message: 'Recipe must have steps!' })
}
const recipe = await Recipe.create({
name: req.body.name,
ingredients: req.body.ingredients,
steps: req.body.steps,
likes: 0,
date: Date.now(),
});
console.log(req.body);
return res.status(200).send(recipe);
}));
update recipe
router.put('/:recipeId',asyncHandler(async function (req, res, next) {
/*
we cannot call recipe = await Recipe.findById(, callback), it will cause "MongooseError:
Query was already executed:"
Mongoose throws a 'Query was already executed' error when a given query is executed twice.
*/
const recipe = Recipe.findById(req.params.recipeId, async (err, foundRecipe) => {
if (err) {
// when the format of input _id is incorrect
return res.status(404).send({ message: 'recipe not found for update' });
} else {
// if _id format is correct but not found, still return a null instead of an error
if (!foundRecipe) res.status(404).send({ message: 'recipe not found for update' });
// we can use update but update() doesn't return the updated recipe
const updRecipe = req.body;
foundRecipe.name = updRecipe.name ? updRecipe.name : foundRecipe.name;
foundRecipe.ingredients = updRecipe.ingredients ? updRecipe.ingredients : foundRecipe.ingredients;
foundRecipe.steps = updRecipe.steps ? updRecipe.steps : foundRecipe.steps;
// put likes just for dubug
foundRecipe.likes = updRecipe.likes ? updRecipe.likes : foundRecipe.likes;
await foundRecipe.save();
console.log(foundRecipe);
return res.status(200).send(foundRecipe);
}
});
}));
delete recipe
router.delete('/:recipeId',asyncHandler(async function (req, res, next) {
const recipe = Recipe.findById(req.params.recipeId, async (err, foundRecipe) => {
if (err) {
// when the format of input _id is incorrect
return res.status(404).send({ message: 'id incorrect for remove' });
} else {
// if _id format is correct but not found, still return a null instead of an error
if (!foundRecipe) res.status(404).send({ message: 'recipe not found for remove' });
await foundRecipe.remove();
console.log(req.params.recipeId);
return res.status(200).json({_id: req.params.recipeId});
}
});
}));
That is how we edit our server side for fetching data from database.
- Workshop4 slide
- mongo DB setup
- mongo enviroment set up
- mongodb tutorial for mac
- mongodb tutorial for windows
- MERN App developemnt
- web security
Because it is complex to deploy frontend and backend in monoropo, I separated them as 2 repo. The frontend repo is here https://github.com/Nick-zhen/RecipeApp-Frontend