ผลต่างระหว่างรุ่นของ "01204223/react-components"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
 
(ไม่แสดง 3 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน)
แถว 251: แถว 251:
  
 
<syntaxhighlight>
 
<syntaxhighlight>
// แก้ในไฟล์ TodoItem.jsx
+
// ** แก้ในไฟล์ TodoItem.jsx **
function TodoItem({todo, toggleDone, deleteTodo, addNewComment}) {
+
function TodoItem({todo, toggleDone, deleteTodo, addNewComment}) {   // เพิ่ม props
  
 
   // .. ละส่วนอื่นไว้
 
   // .. ละส่วนอื่นไว้
แถว 272: แถว 272:
  
 
<syntaxhighlight>
 
<syntaxhighlight>
// แก้ในไฟล์ App.jsx
+
// ** แก้ในไฟล์ App.jsx **
  
 
         {todoList.map(todo => (
 
         {todoList.map(todo => (
แถว 289: แถว 289:
  
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
   // แก้ในไฟล์ App.jsx
+
   // ** แก้ในไฟล์ App.jsx **
 
   async function addNewComment(todoId, newComment) {    // เพิ่ม parameter
 
   async function addNewComment(todoId, newComment) {    // เพิ่ม parameter
 
     try {
 
     try {
แถว 315: แถว 315:
  
 
{{กล่องฟ้า|
 
{{กล่องฟ้า|
ถ้าทุกอย่างใช้งานได้ อย่าลืม commit งานที่ทำด้วย
+
'''🄶''' ถ้าทุกอย่างใช้งานได้ อย่าลืม commit งานที่ทำด้วย
 
}}
 
}}
  
== ทางเลือกอื่น ๆ ==
+
== ประเด็นการออกแบบอื่น ๆ ==
 +
 
 +
จากที่เราได้เขียนมา ยังมีหลายเรื่องที่เราสามารถปรับปรุงได้ แต่หลายเรื่องก็อาจจะไม่จำเป็น ซึ่งในการพัฒนาจริง เราจำเป็นจะต้องชั่งน้ำหนักความสะดวกในการแก้ไข กับกำลังที่เราต้องใส่ไปเพื่อทำอะไรสักอย่าง
 +
 
 +
ตัวอย่างเช่น:
 +
 
 +
* การแยก component ก็ต้องใช้เวลาเขียน ต้องเพิ่มโค้ดการเชื่อมต่อ ถ้าโปรแกรมเล็ก ๆ ก็อาจจะไม่จำเป็น แต่ถ้าโปรแกรมใหญ่ขึ้น การที่โค้ดแยกเป็นส่วนจะทำให้เราสามารถจัดการแก้ไขได้ง่าย
 +
* การอัพเดทในปัจจุบันเราจะ reload ทุก ๆ TodoItem จาก api  ถ้าเรามี TodoItem จำนวนมาก ๆ วิธีการนี้ก็ไม่น่าจะมีประสิทธิภาพเท่าใดนัก เราอาจจะต้องเพิ่มให้โหลด TodoItem ได้เป็นส่วน ๆ ซึ่งในกรณีนี้
 +
* ในปัจจุบันเราเก็บ state ของทุก ๆ TodoItem ไว้ที่ App ผ่านตัวแปร state ตัวเดียว การแก้ไขจะทำให้เกิดการ render ทุก ๆ TodoItem ถ้าเราปรับให้สามารถโหลดหรือแก้ไข TodoItem ได้เป็นอัน ๆ ไป แต่ถ้าการปรับ state ทำผ่าน array เลย การทำงานในส่วน React UI จะซ้ำซ้อนและไม่ค่อยมีประสิทธิภาพ
 +
 
 +
ประเด็นพวกนี้ ถ้าเราจะพัฒนาแอพที่ใหญ่หรือซับซ้อนขึ้น เป็นสิ่งที่เราจำเป็นต้องคำนึงถึงเสมอ  &nbsp;&nbsp;&nbsp; ไม่มีคำตอบที่ใช้ได้กับทุกบริบท หน้าที่ของนักพัฒนาคือการเลือกทางเลือกให้เหมาะสมตามความเข้าใจต่อลักษณะงานที่ทำ

รุ่นแก้ไขปัจจุบันเมื่อ 01:10, 12 กุมภาพันธ์ 2569

หน้านี้เป็นส่วนหนึ่งของวิชา 01204223

ใน React app ที่เราเขียนขึ้น เราใส่ทุกอย่างบน UI ลงใน component App เพียงอย่างเดียว ทำให้โค้ดใน App.jsx มีขนาดใหญ่และซับซ้อนมาก &  nbsp; ข้อสังเกตหนึ่งของความซับซ้อนก็คือการจัดการเกี่ยวกับฟอร์ม comment ของแต่ละ TodoItem ที่เราสร้าง state ที่มีความซับซ้อน

เราจะปรับโครงสร้างของ UI ใหม่ จากที่มี component เดียวดังรูปด้านล่าง

223-react-app-component.png

ให้แยกส่วนแสดง TodoItem ออกเป็นอีก component หนึ่ง ดังรูปต่อไปนี้

223-react-app-component-todoitem.png

การออกแบบระบบดังกล่าวทำให้แต่ละ 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}) {    // เพิ่ม props

  // .. ละส่วนอื่นไว้

      <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);
    }
  }

จากนั้นให้ทดสอบว่า feature ต่าง ๆ ทำงานเรียบร้อย

🄶 ถ้าทุกอย่างใช้งานได้ อย่าลืม commit งานที่ทำด้วย

ประเด็นการออกแบบอื่น ๆ

จากที่เราได้เขียนมา ยังมีหลายเรื่องที่เราสามารถปรับปรุงได้ แต่หลายเรื่องก็อาจจะไม่จำเป็น ซึ่งในการพัฒนาจริง เราจำเป็นจะต้องชั่งน้ำหนักความสะดวกในการแก้ไข กับกำลังที่เราต้องใส่ไปเพื่อทำอะไรสักอย่าง

ตัวอย่างเช่น:

  • การแยก component ก็ต้องใช้เวลาเขียน ต้องเพิ่มโค้ดการเชื่อมต่อ ถ้าโปรแกรมเล็ก ๆ ก็อาจจะไม่จำเป็น แต่ถ้าโปรแกรมใหญ่ขึ้น การที่โค้ดแยกเป็นส่วนจะทำให้เราสามารถจัดการแก้ไขได้ง่าย
  • การอัพเดทในปัจจุบันเราจะ reload ทุก ๆ TodoItem จาก api ถ้าเรามี TodoItem จำนวนมาก ๆ วิธีการนี้ก็ไม่น่าจะมีประสิทธิภาพเท่าใดนัก เราอาจจะต้องเพิ่มให้โหลด TodoItem ได้เป็นส่วน ๆ ซึ่งในกรณีนี้
  • ในปัจจุบันเราเก็บ state ของทุก ๆ TodoItem ไว้ที่ App ผ่านตัวแปร state ตัวเดียว การแก้ไขจะทำให้เกิดการ render ทุก ๆ TodoItem ถ้าเราปรับให้สามารถโหลดหรือแก้ไข TodoItem ได้เป็นอัน ๆ ไป แต่ถ้าการปรับ state ทำผ่าน array เลย การทำงานในส่วน React UI จะซ้ำซ้อนและไม่ค่อยมีประสิทธิภาพ

ประเด็นพวกนี้ ถ้าเราจะพัฒนาแอพที่ใหญ่หรือซับซ้อนขึ้น เป็นสิ่งที่เราจำเป็นต้องคำนึงถึงเสมอ     ไม่มีคำตอบที่ใช้ได้กับทุกบริบท หน้าที่ของนักพัฒนาคือการเลือกทางเลือกให้เหมาะสมตามความเข้าใจต่อลักษณะงานที่ทำ