01204223/react-components
- หน้านี้เป็นส่วนหนึ่งของวิชา 01204223
ใน React app ที่เราเขียนขึ้น เราใส่ทุกอย่างบน UI ลงใน component App เพียงอย่างเดียว ทำให้โค้ดใน App.jsx มีขนาดใหญ่และซับซ้อนมาก & nbsp; ข้อสังเกตหนึ่งของความซับซ้อนก็คือการจัดการเกี่ยวกับฟอร์ม comment ของแต่ละ TodoItem ที่เราสร้าง state ที่มีความซับซ้อน
เราจะปรับโครงสร้างของ UI ใหม่ จากที่มี component เดียวดังรูปด้านล่าง
ให้แยกส่วนแสดง TodoItem ออกเป็นอีก component หนึ่ง ดังรูปต่อไปนี้
การออกแบบระบบดังกล่าวทำให้แต่ละ component เล็กลง แต่ก็แลกมากับการที่ต้องพิจารณาเรื่องการเก็บ state ของ UI และการเก็บ state ของข้อมูลของแอพทั้งหมด
เนื้อหา
แยก Component ในการแสดงผล
แผนการ
ในโค้ด App.jsx ส่วนที่จะถูกตัดออกเป็นจะเป็นส่วนด้านล่างนี้ที่อยู่ใน tag li
{todoList.map(todo => (
<!----------- จะตัดส่วนนี้ไปสร้างอีก component หนึ่ง ------------------->
<li key={todo.id}>
<span className={todo.done ? "done" : ""}>{todo.title}</span>
<button onClick={() => {toggleDone(todo.id)}}>Toggle</button>
<button onClick={() => {deleteTodo(todo.id)}}>❌</button>
{(todo.comments) && (todo.comments.length > 0) && (
// ละไว้
)}
<div className="new-comment-forms">
// ละไว้
</div>
</li>
<!------------------------------------------------------------>
))}เมื่อแยกไปแล้ว ส่วนที่เหลือใน App.jsx จะเป็นดังนี้ แสดงให้ดูเฉย ๆ ยังไม่ต้องแก้ตาม
{todoList.map(todo => (
<!---------------- โค้ดหลังใช้ component TodoItem ---------------->
<TodoItem
key={todo.id}
todo={todo}
toggleDone={toggleDone}
deleteTodo={deleteTodo}
addNewComment={addNewComment}
/>
<!------------------------------------------------------------>
))}การ refactor การแยกโค้ดออกไปเป็นส่วนใหม่ เป็นวิธีการปรับโค้ดเพื่อทำให้มีโครงสร้าง (หรือสถาปัตยกรรม) ดีขึ้น ดูแลรักษาง่ายขึ้น การปรับนี้ เราไม่ได้มีเป้าหมายในการเปลี่ยนแปลงพฤติกรรมของโค้ดเลย เราทำเพื่อปรับโครงสร้างเท่านั้น การดำเนินการนี้ เราจะเรียกว่า การ refactor โค้ด (อ่านเพิ่มใน wiki)
เรียก backend server และเรียก npm run dev
ในขั้นตอนถัดไป เราจะแก้โค้ดไปพร้อม ๆ กับเทส ดังนั้นให้ไปเรียก backend server รวมทั้งเปิด frontend dev ด้วย โดยเรียก
ใน backend ให้ activate virtual environment จากนั้นให้ตั้งค่า FLASK_APP ให้เรียบร้อย แล้วเรียก
flask run --debug
และ ใน frontend
npm run dev
ก่อนจะทำในขั้นถัด ๆ ไป
ตัดโค้ดไปสร้าง TodoItem.jsx
ในการสร้าง TodoItem component เราจะเริ่มจากไฟล์ TodoItem.jsx ในไดเร็กทอรี frontent/src โดยทำเป็นโครงว่าง ๆ ดังด้านล่าง
// ทำในไฟล์ TodoItem.jsx ในไดเร็กทอรี frontent/src/
import './App.css'
function TodoItem({todo}) {
return (
)
}
export default TodoItem
คำอธิบาย
- Component ใน react สมัยใหม่จะเป็น function ที่คืนค่าเป็นก้อน html function นี้จะรับ props (มาจากคำว่า properties) จาก UI component อื่น ๆ โดยในกรณีนี้เราจะรับข้อมูล todo มาจาก component App สังเกตว่าเราเขียน argument todo ภายในวงเล็บปีกกา (สำคัญมาก) ซึ่งจะทำให้เวลามีการส่ง props มาจะมีการแยก todo มาให้โดยอัตโนมัติ
- อย่าลืมบรรทัด export บรรทัดสุดท้าย จะทำให้เราสามารถนำ function นี้ไปใช้จากไฟล์อื่นได้
เราจะเริ่มโดยตัดโค้ดในส่วนของการแสดง TodoItem มาจาก App.jsx โดยโค้ดที่ตัดมาเราจะนำมาเพิ่มในส่วน return ของฟังก์ชัน TodoItem ดังแสดงตัวอย่างด้านล่าง
หมายเหตุ: ให้ตัดโค้ดของตัวเองมา ด้านล่างโค้ดแสดงไม่ครบ
function TodoItem({todo}) {
return (
<li key={todo.id}>
<span className={todo.done ? "done" : ""}>{todo.title}</span>
<button onClick={() => {toggleDone(todo.id)}}>Toggle</button>
<button onClick={() => {deleteTodo(todo.id)}}>❌</button>
{(todo.comments) && (todo.comments.length > 0) && (
<>
// ละไว้
</>
)}
<div className="new-comment-forms">
// ละไว้
<button onClick={() => {addNewComment(todo.id)}}>Add Comment</button>
</div>
</li>
)
}
จากนั้นเราจะไปแก้ App.jsx ให้เรียกใช้ component TodoItem ในการแสดง UI ส่วนนี้
หมายเหตุ: โค้ดที่เราจะแก้ต่อไปตอนแรกจะทำงานได้ไม่ครบ เราจะทยอยแก้ทีละจุดจนกระทั่งทำงานได้
เราจะ import TodoItem มาก่อน ให้เพิ่มบรรทัดด้านบนที่ตอนต้น App.jsx (ต่อท้ายบรรดา import ต่าง ๆ)
// ทำในไฟล์ App.jsx
import TodoItem from './TodoItem.jsx'
จากนั้นในส่วนที่แสดงรายการ Todo ให้แก้ส่วนที่เราตัดออกไปให้เป็น
{todoList.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
/>
))}ข้อสังเกต เราจะยังระบุ prop key ลงไปด้วย ซึ่ง property นี้สำคัญมากสำหรับ react ในการแสดงผล เพราะระบบจะใช้ในการตรวจสอบว่าหน้าจอมีส่วนใดเปลี่ยนแปลงบ้าง
เมื่อแก้เสร็จให้ save ทั้งหมดแล้วกลับไปดูหน้าจอ Todo app ของเรา
จะพบว่าหน้าจอพังไปแล้ว!
ให้ไป Inspect และเปิดดู error ใน console จะพบ error ประมาณด้านล่าง
Uncaught ReferenceError: newComments is not defined
at TodoItem (TodoItem.jsx:22:18)
เราจะทยอยแก้กันไปทีละส่วน
กลับมาแก้ TodoItem.jsx กันต่อ
จาก error เราจะพบว่าใน component เรามีการใช้ state newComments ซึ่งเดิมเราใช้เก็บข้อความในฟอร์ม comment ของทุก ๆ todoitem รวมกัน เนื่องจากตอนนี้เราจัดการแต่ละ component แยกกันแล้ว ดังนั้นเราจะย้าย state ส่วนนี้แยกออกมาไว้ใน component เองเลย การจัดการตรงนี้ทำให้ลดการขึ้นต่อกันระหว่าง component และ App และเป็นประโยชน์หลัก ๆ ของการแยก component ที่เราทำมาทั้งหมด
เราจะเพิ่ม state newComment ลงใน TodoItem และแก้โค้ดเดิมดังนี้
// เพิ่มการ import
import { useState } from 'react'
// .. ละส่วนอื่นไว้
function TodoItem({todo}) {
// เพิ่มบรรทัด
const [newComment, setNewComment] = useState(""); // เพิ่ม state newComment
// .. ละส่วนอื่นไว้
<div className="new-comment-forms">
<input
type="text"
// แก้บรรทัดด้านล่าง
value={newComment} // ของเก่าเป็น value={newComments[todo.id] || ""}
onChange={(e) => {
const value = e.target.value;
// แก้บรรทัดด้านล่าง
setNewComment(value); // ของเดิม: setNewComments({ ...newComments, [todo.id]: value });
}}
/>
<button onClick={() => {addNewComment(todo.id)}}>Add Comment</button>
</div>
// .. ละส่วนอื่นไว้
}ตอนนี้ ถ้า refresh (หรือแค่กด save) หน้าจอ Todo app ของเราน่าจะกลับมาแสดงผลได้แล้ว แต่จะยังไม่สามารถทำงานอะไรได้ เพราะว่าบรรดา callback ของเรา (พวก onClick ต่าง ๆ) ใน TodoItem ยังไม่มี/ยังไม่ได้รับมาจาก App เราจะแก้ในส่วนนี้ในขั้นตอนถัดไป
แต่ก่อนอื่นให้ไปแก้ในส่วน render ของ TodoItem อีกอย่าง คือให้ไปลบ key={todo.id} ออกจาก element li เพราะว่าไม่จำเป็น (เรามักจะใช้ key เวลาที่มีการ render รายการหลาย ๆ หลายการ ที่อาจจะมีการ update เป็นส่วน ๆ)
return (
<li> <!--- ลบ key={todo.id} ออก --->
...
</li>
)ส่ง function สำหรับประมวลผล
ถ้าเราพิจารณาโค้ดของ TodoItem เราจะพบว่ามีการเรียกฟังก์ชันดังต่อไปนี้
- toggleDone
- deleteTodo
- addNewComment
สองฟังก์ชันแรกจะสามารถจัดการได้ง่าย เพราะว่าในการเรียกเราแค่ส่ง todo.id ให้กับฟังก์ชัน แต่ในฟังก์ชัน addNewComment นั้น จะซับซ้อนกว่า ด้านล่างเป็นโค้ดจากใน App.jsx
// ***** เป็นโค้ดเก่า แปะให้อ่านประกอบ ยังไม่ต้องแก้ *******
async function addNewComment(todoId) {
try {
const url = `${TODOLIST_API_URL}${todoId}/comments/`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ 'message': newComments[todoId] || "" }),
});
if (response.ok) {
setNewComments({ ...newComments, [todoId]: "" });
await fetchTodoList();
}
} catch (error) {
console.error("Error adding new comment:", error);
}
}
ถ้าเราอ่านโค้ดใน App.jsx เราจะพบว่าโค้ดเดิมมีการเรียกใช้ newComments จากภายในฟังก์ชัน การเขียนโค้ดในลักษณะดังกล่าวคือการใช้ side effect และจากตัวอย่างนี้เราจะเห็นว่าอาจสร้างความยุ่งยากในการปรับเปลี่ยนได้
เราจะดำเนินการดังนี้
- สำหรับสองฟังก์ชันแรก เราจะแก้ให้มีการส่งฟังก์ชันที่ TodoItem จะเรียก ผ่านทาง props
- สำหรับ addNewComment เราจะแก้ดังนี้
- ใน TodoItem จะส่ง newComment ที่เป็นข้อความไปให้ฟังก์ชันด้วย
- ย้ายการ setNewComments ให้ไปเรียกใน TodoItem (เพราะว่าตอนนี้สถานะของค่าในฟอร์มเก็บที่ TodoItem แล้ว)
เราจะเริ่มจากการแก้ใน TodoItem.jsx ก่อน
// แก้ในไฟล์ TodoItem.jsx
function TodoItem({todo, toggleDone, deleteTodo, addNewComment}) {
// .. ละส่วนอื่นไว้
<div className="new-comment-forms">
// .. ละไว้
<button onClick={() => { // แก้ส่วนนี้
addNewComment(todo.id, newComment);
setNewComment("");
}}>Add Comment</button>
</div>
// .. ละส่วนอื่นไว้
}จากนั้นไปแก้ App.jsx โดยเริ่มจากการส่งฟังก์ชันให้กับ TodoItem
// แก้ในไฟล์ App.jsx
{todoList.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
toggleDone={toggleDone}
deleteTodo={deleteTodo}
addNewComment={addNewComment}
/>
))}และแก้ฟังก์ชัน addNewComment ให้เป็นดังด้านล่าง
// แก้ในไฟล์ App.jsx
async function addNewComment(todoId, newComment) { // เพิ่ม parameter
try {
const url = `${TODOLIST_API_URL}${todoId}/comments/`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ 'message': newComment }), // ใช้ newComment
});
if (response.ok) {
//
// ****** ลบบรรทัด setNewComments({ ...newComments, [todoId]: "" }); *******
//
await fetchTodoList();
}
} catch (error) {
console.error("Error adding new comment:", error);
}
}

