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

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
แถว 193: แถว 193:
 
   // .. ละส่วนอื่นไว้
 
   // .. ละส่วนอื่นไว้
 
}
 
}
 +
</syntaxhighlight>
 +
 +
ตอนนี้ ถ้า refresh (หรือแค่กด save) หน้าจอ Todo app ของเราน่าจะกลับมาแสดงผลได้แล้ว แต่จะยังไม่สามารถทำงานอะไรได้ เพราะว่าบรรดา callback ของเรา (พวก onClick ต่าง ๆ) ใน TodoItem ยังไม่มี/ยังไม่ได้รับมาจาก App  เราจะแก้ในส่วนนี้ในขั้นตอนถัดไป
 +
 +
แต่ก่อนอื่นให้ไปแก้ในส่วน render ของ TodoItem อีกอย่าง คือให้ไปลบ key={todo.id} ออกจาก element li เพราะว่าไม่จำเป็น (เรามักจะใช้ key เวลาที่มีการ render รายการหลาย ๆ หลายการ ที่อาจจะมีการ update เป็นส่วน ๆ)
 +
 +
<syntaxhighlight>
 +
  return (
 +
    <li>          <!--- ลบ key={todo.id} ออก --->
 +
      ...
 +
    </li>
 +
  )
 
</syntaxhighlight>
 
</syntaxhighlight>
  

รุ่นแก้ไขเมื่อ 19:39, 11 กุมภาพันธ์ 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>
  )

ส่ง callback

ทางเลือกอื่น ๆ