-
Notifications
You must be signed in to change notification settings - Fork 0
Project 5 Frontend Backend Integration
ลองทำการเชื่อมต่อ Frontend กับ Backend ของเราให้ทำงานต่อเนื่องกันได้
- ในหน้าแรก App.js ลองสร้างลิสต์ของผู้ใช้งาน โดยดึงข้อมูลจาก Backend เปลี่ยนการดึงข้อมูลจาก axios ทึ่เคยดึง public api ให้เป็นการดึงไปที่
/api/userใน backend เปลี่ยนชื่อสเตทเป็นusersกับsetUsersเพื่อการสื่อความหมายที่ดียิ่งขึ้น เราตั้งของการ return ออกมา เราให้ลิสต์ของค่าอยู่ที่ .rows เราจะปรับโค้ดใน useEffect เป็นดังนี้
const [users, setUsers] = useState([]);
useEffect(() => {
axios
.get("http://localhost:3001/api/user")
.then((res) => {
setUsers(res?.data?.rows);
console.log("User", res?.data?.rows);
})
.catch((error) => {
console.error("Error", error?.message);
});
return () => {};
}, []);2.ลองดู Log จะพบว่ามีมันมีปัญหา Cross Origin Error เนื่องจากว่ามันมีนโยบายที่เรียกว่า CORS Policy ในการป้องกันไม่ให้บราวเซอร์เครื่องอื่น หรือ โดเมนอื่นเข้าถึงได้ อย่างตอนนี้ react เข้าผ่าน localhost:3000 ส่วนตัว backend เรารันอยู่ที่ localhost:3001 ซึ่งพวกนี้จะเกิดขึ้นตอนเรา Request บน Code ที่รันผ่าน Browser ตอนเราใช้แบบใส่ URL ไว้บน Address Bar เฉยๆ หรือบนโปรแกรมพวก postman หรืออื่นๆ มันจะไม่มีผล เราจะต้องแก้ปัญหาโดยการอนุญาติให้เข้าถึงได้ โดยเบื้องต้นเราจะอนุญาติสำหรับทุกๆ endpoint ก่อน วิธีการทำคือ เข้าไปที่ backend
cd backend
npm install --save corsจากนั้นในไฟล์ index.js ของ backend เราจะใส่
const cors = require("cors");
app.use(cors({}));-
กลับมา Refresh ที่ Frontend อีกครั้ง จะพบว่าเราสามารถ Get ข้อมูลออกมาได้แล้ว

-
ทำการ map ข้อมูลที่อยู่ใน State ของ users มาแสดง โดยในที่นี้จะใช้ lodash.map (
_.map) หรืออาจจะใช้users.mapก็ได้
<div>
<h3 className='font-bold'>User List</h3>
{_.map(users, (eachUser, index) => (
<div>
{index + 1}. - {eachUser?.name}
</div>
))}
</div>ผลที่ได้จะออกมาประมาณนี้

เนื่องจากบางทีการโหลดข้อมูลเข้ามาอาจจะใช้เวลา มันอาจจะไม่เหมาะให้ผู้ใช้งานรอเฉยๆ สิ่งหนึ่งที่นิยมใช้กันคือการแสดง Loading 5. สร้าง state ขึ้นมาว่า ready หรือไม่
const [isReady,setIsReady] = useState(false)
แล้วสร้างผลการ Return ออกมาใน 2 รูปแบบแทนที่เราจะ return ออกมาแบบเดียว เปลี่ยนเป็น ถ้า isReady เป็น false เราให้แสดงอย่างอื่น อาจจะไป Import Component ที่เป็น Loading หรือ Progress Bar มาก็ได้
const [isReady, setIsReady] = useState(false);
if (!isReady) {
return (
<div>
<LinearProgress />
</div>
);
}
return (
<div>
<Topbar appTitle='IARC Devboard' />{" "}
<div className='min-h-screen'>
<div className='flex justify-center '>
<div className='lg:w-3/4'>
<Card>
......ผลจะออกมาเป็นแบบนี้เพราะเรายังไม่มีการอัพเดท state ของ isReady

- ตั้งค่าให้ setIsReady เป็นtrue เมื่อมีการดึงข้อมูลมาได้แล้ว
useEffect(() => {
axios
.get("http://localhost:3001/api/user")
.then((res) => {
setUsers(res?.data?.rows);
setIsReady(true);
console.log("User ", res?.data?.rows);
})
.catch((error) => {
console.error("Error", error?.message);
});
return () => {};
}, []);ทุกอย่างก็จะกลับมาเหมือนเดิม
- ลอง จัด UI ให้เป็นตาราง หรือ การแสดงผลแบบที่ต้องการ และใส่ปุ่ม Delete ให้ในแต่ละ Record โดยอาจจะดึงจาก UI Library มาก็ได้ ตัวอย่างเช่น
<div>
<h3 className='font-bold'>User List</h3>
<Table>
<thead>
<tr>
<th>ลำดับที่</th>
<th>ชื่อ</th>
<th>แผนก</th>
<th>ดำเนินการ</th>
</tr>
</thead>
{_.map(users, (eachUser, index) => (
<tr>
<td>{index + 1}</td>
<td>{eachUser?.name}</td>
<td>{eachUser?.department}</td>
<td>
<Button color='danger'>ลบ</Button>
</td>
</tr>
))}
</Table>
</div>จะได้ผลออกมาประมาณนี้

- สร้างฟังก์ชันเตรียมสำหรับการคลิกปุ่มและส่ง Parameter เป็น ID ของ Record เข้ามา
const handleDeleteUser = (userId)=>{
}และ
<td>
<Button color='danger' onClick={()=>handleDeleteUser(eachUser?._id)}>ลบ</Button>
</td>- หยิบโค้ดใน Use Effect ออกมาเป็นเป็นฟังก์ชันใหม่ เพื่อเราจะได้ Reuse โค้ดได้ง่ายขึ้น โดยเราจะเริ่มจากตั้ง isReady เป็น false ก่อน
const getAllUser = () => {
setIsReady(false)
axios
.get("http://localhost:3001/api/user")
.then((res) => {
setUsers(res?.data?.rows);
setIsReady(true);
console.log("User ", res?.data?.rows);
})
.catch((error) => {
console.error("Error", error?.message);
});
};
useEffect(() => {
getAllUser();
return () => {};
}, []);- ปรับปรุงฟังก์ชันสำหรับการ Delete User ให้มีการส่ง DELETE ไปที่ Backend แล้วก็ Get อีกครั้ง
const handleDeleteUser = (userId) => {
axios
.delete("http://localhost:3001/api/user/" + userId)
.then((res) => {
getAllUser();
})
.catch((error) => {
alert(error?.message);
console.error("Error", error?.message);
});
};
โค้ดในฝั่ง Backend
router.delete("/:id", async (req, res) => {
console.log("Delete Users");
try {
const result = await User.findByIdAndDelete(req?.params?.id);
res.status(204).json(result);
} catch (error) {
res.status(404).json({ err: error });
}
});ผลเมื่อลบรายการที่ 5 ออกไป

เนื่องจากเรามีการเขียน URL หลายครั้ง แล้ว Base URL มันเหมือนกันหมด มีความหมายถึง URL เหมือนกัน เราจะสร้างเป็น Environment Variable เพื่อสะดวกกับการใช้งาน และเราจะเปลี่ยนมันง่ายด้วย
- สร้าง .env ไว้ใน root โฟลเดอร์ของ frontend โดยมีข้อจำกัดคือ Environment Variable ของ React จะต้องขึ้นต้นด้วย
VITE_โดยเราจะสร้างว่า VITE_API_URL

VITE_API_URL=http://localhost:3001/api
จากนั้นเข้ามาเปลี่ยนโค้ดใน App.js จาก "http://localhost:3001/api/user" เป็น ` ${import.meta.env.VITE_API_URL}/user ` หรือจะใช้ import.meta.env.VITE_API_URL + '/user' ก็ได้
const getAllUser = () => {
setIsReady(false);
axios
.get(`${import.meta.env.VITE_API_URL}/user`)
.then((res) => {
setUsers(res?.data?.rows);
setIsReady(true);
console.log("User ", res?.data?.rows);
})
.catch((error) => {
console.error("Error", error?.message);
});
};
- ทำการ Restart Development Server ในส่วนของ Frontend (Ctrl+C และ npm start ใหม่) แล้วลองดูผลลัพธ์ ถ้าถูกต้องจะออกมาเหมือนเดิม
หมายเหตุ การเขียนใน axios เขียนแบบ .then .catch เรียกว่าเขียน Promise แต่เราสามารถเขียนในแบบของ async await และ try/catch ได้ จะได้ผลเหมือนกัน
const getAllUser = async () => {
setIsReady(false);
try {
const res = await axios.get(`${import.meta.env.VITE_API_URL}/user`);
setUsers(res?.data?.rows);
setIsReady(true);
console.log("User ", res?.data?.rows);
}
catch(error){
console.error("Error", error?.message);
}
};ในการสร้างฟอร์มสำหรับการสร้างหรือแก้ไข Record เข้าไปใน Collection ของฐานข้อมูลนั้น มีหลายวิธี วิธีที่เป็นพื้นฐานที่สุดก็ตือ การใช้ state จับทุกๆ ครั้งที่มีการเปลี่ยนข้อความใน Text Box หรือ Form Element อื่น ๆ แต่มันจะมี Library ตัวหนึ่งที่สามารถจัดการได้ดียิ่งขึ้นก็คือ React Hook Form ใน React Hook Form ซึ่งก็เขียน 2 รูปแบบคือการเขียนแบบ Register และการเขียนแบบ Controller โดยในที่นี้จะสอนใช้แบบ Controller ก่อน
- ลง Library ของ react-hook-form แล้ว Import เข้ามาในไฟล์ App.js
npm install --save react-hook-form
ในไฟล์ App.js ดึง useForm และ Controller จาก react-hook-form ออกมาจากนั้นนำ controller ออกมาจาก useForm
import { useForm,Controller } from "react-hook-form";
function App(){
const { control } = useForm();
...
}- สร้าง Tag
<form></form>เข้ามา แล้วเริ่มสร้าง Form Element สำหรับการรับชื่อ (attribute ชื่อ name) โดยเริ่มต้นจากการสร้างเป็น Component Controller ตั้งไว้ ใน Component ของ Controller จะมี attribute ที่จำเป็นต้องกรอก 3 ตัวอันได้แก่
- name จะไปเป็น key ของค่า
- control ต้องใส่ control object
- render ใส่ HTML หรือ JSX Element สำหรับสร้างฟอร์มในการรับข้อมูล ดังนี้ (ใน Return)
<div className='lg:w-3/4 '>
<div className='my-1 font-semibold text-lg'>เพิ่มพนักงานใหม่</div>
<Card>
<CardContent>
<form>
<div>ชื่อ</div>
<Controller
name='name'
control={control}
render={({ field }) => (
<Input {...field} placeholder='ชื่อพนักงาน' />
)}
/>
</form>
</CardContent>
</Card>
</div>
จะสังเกตุว่าใน render จะมีฟิลด์ที่ชื่อว่า field อยู่ ใน field จะมีสิ่งต่าง ๆ ได้แก่
- onChange สำหรับเปลี่ยนค่าในฟอร์ม
- value คือค่าที่อยู่ในฟอร์ม
- defaultValue คือค่าเริ่มต้นของฟอร์ม
เราจะใช้หลักการของ spread operator หรือที่เขียนว่า ...field คือการเปลี่ยนแปลง เอา attribute ของ field มารวมกับ attribute หรือ props ของ Input
Input (หรือว่า TextField ใน MUI Material) ปกติจะมี attribute เช่น value, onChange, placeholder, defaultValue อยู่แล้ว ไม่ว่าจะ library ไหน ๆ การที่เรา spread operator ออกมา attribute จาก field ไม่ว่าจะเป็น onChnage, value,defaultValue จะไปชนกับ attribute ของ UI Component พอดี
- ใส่ปุ่มเข้ามา 1 ปุ่มภายใต้ form และตั้งให้เป็น type = summit
<form>
<div>ชื่อ</div>
<Controller
name='name'
control={control}
render={({ field }) => (
<Input {...field} placeholder='ชื่อพนักงาน' />
)}
/>
<div>
<Button type="submit">บันทึก</Button>
</div>
</form>- ใน useForm() ดึง attribute ออกมาอีกตัวคือ handleSubmit
const { control,handleSubmit } = useForm();จากนั้นสร้างฟังก์ชันตัวหนึ่งที่มี Parameters 1 ตัว
const { control,handleSubmit } = useForm();
const handleCreateUser = (data) => {
console.log("data", data);
};- ใส่ attribute ใน เพิ่ม onSubmit ขึ้นมา โดยโยนให้มันส่งมาที่ฟังก์ชัน
handleSubmitของ React Hook Form และให้ handleSubmit ส่งต่อไปที่ฟังก์ชัน handleCreateUser
<form onSubmit={handleSubmit(handleCreateUser)}>
<div>ชื่อ</div>
<Controller
name='name'
control={control}
render={({ field }) => (
<Input {...field} placeholder='ชื่อพนักงาน' />
)}/>
<div>
<Button type="submit">บันทึก</Button>
</div>
</form>- ลองพิมพ์ข้อมูลลงในฟอร์มแล้วกดปุ่ม บันทึก แล้วลองดู Log ที่มันเกิดขึ้น
จะพบว่า name ที่ใส่ใน จะไปเป็น key ใน data ที่ได้ออกมา - เพิ่ม Form Element อื่นๆ เข้ามา อาจจะเป็น Text ด้วยกัน หรือว่าเป็นพวก Checkout เกิดอยู่
<form onSubmit={handleSubmit(handleCreateUser)}>
<div>ชื่อ</div>
<Controller
name='name'
control={control}
render={({ field }) => (
<Input {...field} placeholder='ชื่อพนักงาน' />
)}
/>
<div>แผนก</div>
<Controller
name='department'
control={control}
render={({ field }) => (
<Input {...field} placeholder='แผนก' />
)}
/>
<div>
<Button type='submit'>บันทึก</Button>
</div>
</form>- ใส่ฟังก์ชันการเพิ่มใน handleCreateUser เพื่อนำข้อมูลนี้ส่งไปที่ Backend เพื่อให้ส่งไปที่ Backend แล้วเราจะ GET กลับมาอีกครั้งเพื่อให้ได้ข้อมูลใหม่มา
const handleCreateUser = (data) => {
console.log("data", data);
setIsReady(false);
axios
.post(`${process.env.REACT_APP_API_URL}/user`, data)
.then((res) => {
axios.get(`${process.env.REACT_APP_API_URL}/user`).then((res) => {
setUsers(res?.data?.rows);
setIsReady(true);
});
})
.catch((error) => {
console.error("Error", error?.message);
});
};