Client-Side Interactions & State Management

UI คือภาพสะท้อนของข้อมูล — เราไม่ลบแถวออกจาก HTML แต่ลบ item ออกจาก array แล้วสั่ง render ใหม่

HTML vs UI vs DOM

คืออะไรสถานะ
HTMLโครงสร้าง — บอกว่ามีเนื้อหาอะไรบ้าง (heading, paragraph, button)static
UIประสบการณ์ — ผลลัพธ์ทางสายตาหลังใส่ CSS, JS และหลักการออกแบบสิ่งที่ผู้ใช้เห็นและสัมผัส
DOMตัวแทนที่ "มีชีวิต" — เบราว์เซอร์ parse HTML เป็นโครงสร้างต้นไม้ของ object ในหน่วยความจำdynamic

DOM Tree

html head body title "My Document" h1 p "Header" "Paragraph" ทุกแท็กกลายเป็น node ที่ JavaScript เข้าถึงและแก้ไขได้ และแขวน event handler ได้

เบราว์เซอร์อ่าน HTML แล้วสร้าง DOM ขึ้นมาในหน่วยความจำ จากนั้นจึงเริ่ม "ฟัง" เหตุการณ์

<html lang="en">
  <head><title>Accessing DOM</title></head>
  <body>
    <script>
      const heading = document.createElement("h1");
      const headingText = document.createTextNode("Big Head!");
      heading.appendChild(headingText);
      document.body.appendChild(heading);
    </script>
  </body>
</html>

Event-Driven Architecture (EDA)

ส่วนประกอบของระบบสื่อสารกันด้วยการ "ผลิต" และ "ตอบสนอง" ต่อ event

ตัวอย่างเชิงธุรกิจ

ลูกค้าสั่งซื้อ → เกิด event Order Placed → service ชำระเงิน, จัดการสต็อก และส่งอีเมล ไม่ต้องคอยถามระบบสั่งซื้อตลอดเวลา แต่ตอบสนองเองเมื่อ event เกิด

บทบาทคืออะไรตัวอย่าง
Producer (ตัวกระตุ้น)element ที่เป็นจุดเริ่มของ actionปุ่ม "Buy Now", ช่องค้นหา
Event (ข้อความ)object ที่บรรจุข้อมูลว่าเกิดอะไรขึ้นกดปุ่มไหน ค่าปัจจุบันของ dropdown คืออะไร
Consumer (ตัวจัดการ)ฟังก์ชัน JS ที่รัน business logicเพิ่มสินค้าเข้า array ของตะกร้า

Event Loop

Event Delegation

แบบสิ้นเปลือง: ผูก listener ทุกปุ่ม #catalog btn 1 btn 2 btn 3 btn 100 100 listeners = 100 หน่วยความจำ Event Delegation: ผูกที่ parent ตัวเดียว #catalog ← 1 listener btn 1 btn 2 btn 3 btn 100 event bubble ขึ้นมาหา parent ตรวจว่าใครถูกคลิกด้วย event.target.closest('.add-to-cart') แล้วอ่าน data-id ข้อดี: สินค้าเพิ่มกี่ชิ้นก็ได้ หน่วยความจำเท่าเดิม และปุ่มที่เพิ่มทีหลังก็ทำงานทันที

Business logic: ทำให้แอป scale ได้ — catalog โตขึ้นแต่ memory footprint เท่าเดิม

document.getElementById('catalog').addEventListener('click', function(event) {
    const btn = event.target.closest('.add-to-cart');
    if (!btn) return;                      // คลิกโดนที่ว่าง ไม่ต้องทำอะไร
    const id = btn.dataset.id;             // อ่าน data-id ของปุ่มที่ถูกคลิก
    addToCart(id);
});

Debouncing

ช่องค้นหาที่กรองทุกการกดแป้นอาจยิง 10 requests ใน 2 วินาที — แก้ด้วยการรอให้ผู้ใช้หยุดพิมพ์ 300msก่อนค่อยทำงาน

let timer;
searchBox.addEventListener('input', function(e) {
    clearTimeout(timer);                        // ยกเลิกนัดเดิม
    timer = setTimeout(function() {
        filterProducts(e.target.value);         // ทำจริงเมื่อเงียบครบ 300ms
    }, 300);
});
ประโยชน์: ประหยัดค่า server และทำให้ประสิทธิภาพดีขึ้น

State เป็น Single Source of Truth

  1. UI คือภาพสะท้อนของ state — ไม่ลบแถวใน HTML แต่ลบ item ออกจาก array แล้ว render ใหม่ ถ้าข้อมูลถูก UI จะถูกตาม
  2. แยก logic ออกจาก trigger — filterProducts() ไม่ควรสนใจว่าถูกเรียกจาก input box หรือ dropdown ทำให้นำกลับมาใช้ซ้ำและเทสต์ง่าย
  3. เผื่อ edge case — ถ้าค้นแล้วไม่เจอ ต้องมี state "No results found" ไม่ใช่ปล่อยหน้าว่าง

Logic table จากสไลด์

User ActionDOM EventBusiness LogicUI Change
คลิก "Price: High to Low"changesort allProducts ตามราคาcatalog เรียงใหม่ทันที
พิมพ์ "Nike" ในช่องค้นหาinputfilter array หา "Nike"สินค้าที่ไม่ตรงหายไป
คำถามชวนคิดในสไลด์: logic รองรับ case sensitivity หรือไม่ — ค้น "shoes" กับ "Shoes" ต้องได้ผลเหมือนกัน (ใช้ toLowerCase() ทั้งสองฝั่ง)

localStorage / sessionStorage / cookies

FeaturelocalStoragesessionStorageCookies
Persistenceจนกว่าจะลบเองจนกว่าจะปิดแท็บตามที่ตั้งค่า
Scopeใช้ร่วมทุกแท็บของ origin เดียวกันเฉพาะแท็บที่สร้างคุมด้วย Domain และ Path
Capacity~5–10MB~5MB~4KB
Data TypesStringStringString
APIง่ายง่ายปานกลาง
localStorage.setItem('theme', 'dark');        // เพิ่ม/ทับค่า
const theme = localStorage.getItem('theme');  // อ่านค่า
localStorage.removeItem('theme');             // ลบเฉพาะ key
localStorage.clear();                         // ล้างทั้ง origin

Serialization Bridge

localStorage เก็บได้แค่ string จึงต้องแปลงไปกลับ

localStorage.setItem('cart', JSON.stringify(cartArray));   // Saving  : object → string
const cart = JSON.parse(localStorage.getItem('cart'));      // Loading : string → object
สถานะข้อมูลอยู่ที่ไหนลักษณะ
In-Memory (Live)ตัวแปร JS ระหว่างแอปทำงานเร็ว แต่ volatile — refresh แล้วหาย
On-Disk (Stored)localStorageอยู่ข้ามการ refresh แต่เป็น string ล้วน

State Rehydration — ลำดับตอนเปิดหน้า

  1. Check — มี key shopping_cart ใน localStorage ไหม
  2. Validate — ข้อมูลที่ได้เป็น array ที่ใช้ได้จริงไหม (เผื่อข้อมูลเสีย)
  3. Sync — ตั้งค่าตัวแปร global cartState ให้เท่ากับข้อมูลที่บันทึกไว้
  4. Render — เรียกฟังก์ชันอัปเดต UI ทันที ให้ผู้ใช้เห็นของในตะกร้าตั้งแต่หน้าโหลดเสร็จ
ความสอดคล้อง (Consistency): เพิ่มของ 1 ชิ้น มี 3 จุดต้องอัปเดต — sidebar ตะกร้า, ตัวเลขบน navbar, ยอดรวมหน้า checkout วิธีที่ถูกคือเขียน state observer ตัวเดียวที่อัปเดต cartState แล้วสั่ง refresh ทุกส่วน ไม่ใช่เขียนฟังก์ชันอัปเดตแยก 3 ตัว
Continuity vs Persistence: Continuity = ไม่ให้ข้อมูลหายตอน refresh (localStorage) · Persistence = เข้าถึงตะกร้าจากอุปกรณ์อื่นได้ (ต้องเก็บใน database)

ความเสี่ยง XSS ของ localStorage

ห้ามเก็บข้อมูลอ่อนไหวใน localStorage เด็ดขาด — เช่น รหัสผ่านหรือ authentication token
เพราะ JavaScript ทุกตัวที่รันบนหน้านั้นเข้าถึง localStorage ได้ทั้งหมด ถ้าเว็บมีช่องโหว่ XSS (Cross-Site Scripting) สคริปต์ของผู้โจมตีจะอ่าน token ไปได้ทันที

ทางลด: sanitize ข้อมูลก่อนเอาไปแสดงผลใน DOM, ใช้ cookie แบบ HttpOnly สำหรับ token ที่อ่อนไหว และตั้งอายุ token ให้สั้น

แนวข้อสอบอัตนัย

โจทย์: เว็บร้านค้าเก็บ JWT ไว้ใน localStorage และมีช่องรีวิวสินค้าที่แสดงข้อความผู้ใช้ด้วย innerHTML จงอธิบายปัญหา สาเหตุ ผลกระทบ และวิธีแก้

  1. ปัญหา — ผู้โจมตีโพสต์รีวิวที่มี <script> ซึ่งจะถูกรันในเบราว์เซอร์ของผู้ใช้คนอื่น แล้วขโมย token จาก localStorage
  2. สาเหตุทางเทคนิค — สองอย่างประกอบกัน: (ก) ข้อมูลจากผู้ใช้ถูกเขียนเข้า DOM ผ่าน innerHTML โดยไม่ sanitize ทำให้เกิด XSS (ข) token ถูกเก็บใน localStorage ซึ่ง JavaScript ทุกตัวบนหน้าอ่านได้
  3. ผลกระทบ — ผู้โจมตีสวมสิทธิ์ผู้ใช้ (session hijacking) สั่งซื้อหรือดูข้อมูลส่วนตัวได้ กระทบทุกคนที่เปิดหน้ารีวิวนั้น
  4. วิธีแก้เชิงสถาปัตยกรรม — แสดงข้อความผู้ใช้ด้วย textContent แทน innerHTML และ sanitize/escape ข้อมูลทั้งฝั่ง server ก่อนบันทึกและฝั่ง client ก่อนแสดง, ย้าย token ไปเก็บใน cookie แบบ HttpOnly; Secure; SameSite, ตั้งอายุ token สั้นและมี refresh token