How to read Svelte you did not write
ทุกโมดูลก่อนหน้านี้สอนให้คุณ เขียน Svelte โมดูลนี้สอนอีกครึ่งหนึ่ง: การอ่าน Svelte ที่คนอื่น — หรือ AI — เขียนไว้ แล้วตัดสินว่ามันถูกต้องหรือไม่ ผู้ช่วย AI ยื่นคอมโพเนนต์ที่ compile ผ่าน, svelte-check ยอมรับ, และหน้าตาเหมือนตัวอย่างที่คุณเพิ่งเรียนมาทุกประการ สิ่งเหล่านั้นไม่ได้พิสูจน์ว่ามันถูกต้องเลย compiler ของ Svelte เก่งมากในการจับ error เชิง syntax แต่เงียบผิดปกติกับ error เชิง พฤติกรรม: $effect ที่ mirror state ช้าไปหนึ่งจังหวะ, {#each} ที่สลับ state ภายในของสองแถวโดยไม่มีใครรู้, form action ที่คืนรูปร่างถูกต้องแต่ HTTP status ผิด หกขั้นตอนด้านล่างคือลำดับที่เผยความผิดพลาดเหล่านี้ออกมาจริง ๆ และทุกบทถัดไปในโมดูลนี้จะเจาะลึกส่วนหนึ่งของมัน
flowchart TD
A["1. Runes vs legacy mode<br/>which mode is this file even in?"] --> B["2. State ownership<br/>$state vs $derived vs $effect, module state"]
B --> C["3. Props, bindings, snippets<br/>$bindable, {@render}"]
C --> D["4. Events and DOM<br/>onclick, delegation, attachments vs actions"]
D --> E["5. The SvelteKit boundary<br/>server vs universal load, actions, hooks, env"]
E --> F["6. Keys, transitions, boundaries"]
1. Runes vs legacy mode: ไฟล์นี้อยู่ mode ไหนกันแน่
หัวข้อที่มีชื่อว่า “1. Runes vs legacy mode: ไฟล์นี้อยู่ mode ไหนกันแน่”ก่อนจะเชื่ออะไรอื่นในไฟล์ .svelte ต้องหาก่อนว่าไฟล์นี้เขียนด้วยระบบ reactivity ไหนใน 2 ระบบที่เข้ากันไม่ได้ของ Svelte compiler ตัดสินแบบ ต่อไฟล์ โดยอัตโนมัติ จากเนื้อหาของไฟล์นั้นเอง: ทันทีที่มี rune ใดก็ตาม ($state, $derived, $effect, $props, …) ปรากฏที่ไหนก็ตามในไฟล์ ไฟล์ทั้งไฟล์จะเข้าสู่ runes mode และกลไก reactivity ของ Svelte 4 (export let, $: แบบ top-level) จะกลายเป็น compile error จริง ไม่ใช่แค่ทำงานต่างออกไป ผู้ช่วย AI ที่ฝึกจากตัวอย่างผสมทั้ง Svelte 4 และ Svelte 5 มักผลิตไฟล์ที่เริ่ม migrate เป็น runes mode แล้วหยุดกลางทาง — เป็นรูปแบบบั๊กที่พบบ่อยที่สุดของ Svelte ที่ AI เขียน
- ต้องมองหาอะไร: ไฟล์นี้ผสม rune เข้ากับ
export let,$:แบบ top-level,<slot>, event directive แบบon:click, หรือcreateEventDispatcherหรือไม่? สองอย่างแรกเป็น compile error จริง ทันทีที่มี rune อยู่ในไฟล์; สองอย่างกลางเป็น deprecation warning จริงแต่เบากว่า; อย่างสุดท้าย compile ผ่านโดยไม่มีสัญญาณใด ๆ เลย การรู้ว่ารูปแบบเก่าแบบไหนอยู่กลุ่มไหน เปลี่ยนความเร่งด่วนในการแก้ไข - คำถามที่ต้องถาม: ถ้า compile ไฟล์นี้ตอนนี้เลย กลไก reactivity (
export let,$:) จะยังถูกกฎหมายในไฟล์นี้หรือไม่ — และแยกกัน syntax (<slot>,on:click) แค่ล้าสมัย หรือ สำนวน (createEventDispatcher) แค่ไม่ทันสมัยเท่านั้น? - บทไหนอธิบายเรื่องนี้: Why Svelte, the Compiler พิสูจน์การตรวจจับ mode ต่อไฟล์ตรง ๆ ด้วย
metadata.runesของcompile(); Events & Attributes มีตารางจดจำโค้ด legacy ครบทั้ง 4 รูปแบบพร้อมข้อความ error/warning จริง; AI Bug Catalog กลุ่มแรกไล่ทั้ง 4 แบบเทียบกัน
2. ใครเป็นเจ้าของ state แต่ละชิ้น
หัวข้อที่มีชื่อว่า “2. ใครเป็นเจ้าของ state แต่ละชิ้น”การประกาศ $state ทุกครั้งคือคำกล่าวอ้างเรื่องความเป็นเจ้าของ: นี่คือแหล่งความจริงหนึ่งเดียวของค่านี้ คำนวณครั้งเดียวและอ่านได้ทุกที่ที่ต้องใช้ โค้ดที่ AI เขียนมักสร้างความเป็นเจ้าของซ้ำซ้อนโดยไม่ตั้งใจ — mirror prop เข้า $state แล้ว sync ด้วย $effect แทนที่จะ derive, เขียนค่าที่คำนวณได้ไว้ใน $effect แทนที่จะเป็น $derived, หรือ export $state ระดับโมดูลที่ถูกแชร์เงียบ ๆ โดยทุกคำขอบนเซิร์ฟเวอร์
- ต้องมองหาอะไร:
$effectที่ทำแค่ assign ค่าให้ตัวแปร$stateอีกตัว (เกือบทุกครั้งควรเป็น$derived); ค่าที่อ่านภายในsetTimeout/await/callback ใด ๆ ที่รันหลังจาก$effectหรือ$derivedที่ล้อมรอบมันจบรอบ synchronous ไปแล้ว (ไม่ใช่ dependency ที่ถูก track ไม่ว่าจะดู reactive แค่ไหน);$state.rawที่ mutate ตรง ๆ แทนที่จะ reassign; โมดูล.svelte.ts/.svelte.jsที่ export$stateซึ่งถูกเขียนทับภายในloadfunction หรือ request handler - คำถามที่ต้องถาม: ค่านี้เป็น
$derivedจากสิ่งที่มันพึ่งพาอยู่แล้วได้หรือไม่ — และถ้า module-level state ของไฟล์นี้ถูก mutate บนเซิร์ฟเวอร์ มันปลอดภัยสำหรับทุกคำขอที่เกิดพร้อมกันหรือไม่ หรือมันรั่วข้อมูลของผู้ใช้คนหนึ่งไปให้อีกคน? - บทไหนอธิบายเรื่องนี้: The
$effectRune พิสูจน์ความล่าช้าของ effect-mirror ด้วยเทสต์จริง; The$derivedRune พิสูจน์ push-pull laziness และเหตุผลที่$derivedต้องปราศจาก side effect; The$stateRune พิสูจน์กับดักการ mutate เงียบ ๆ ของ$state.rawและการรั่วของ module-state ตอน SSR โดยตรง
3. Props, bindings และสัญญาของ snippet
หัวข้อที่มีชื่อว่า “3. Props, bindings และสัญญาของ snippet”$props(), $bindable(), และ {#snippet}/{@render} แต่ละอันมีสัญญาที่ผิดครึ่งเดียวได้ง่าย: prop ไหลทางเดียวเว้นแต่ child จะเลือกเปิด two-way binding เอง และ snippet เป็นค่าที่ต้องถูก render ไม่ใช่เรียกเหมือนฟังก์ชัน โค้ดที่ AI เขียนมักสมมติสัญญานี้โดยไม่ตรวจสอบ — bind ไปยัง prop ที่ child ไม่เคยประกาศ bindable หรือเขียนชื่อ snippet เป็นการเรียกเปล่า ๆ
- ต้องมองหาอะไร: parent ทำ
<Child bind:value={x} />ทั้งที่Childไม่เคยเขียนlet { value = $bindable() } = $props(); snippet ที่ถูกอ้างเป็น{name(args)}แทนที่จะเป็น{@render name(args)};{#snippet}ที่ประกาศไว้แต่ไม่เคยถูก render ที่ไหนเลย;use:action ที่ argument เปลี่ยนหลัง mount โดยไม่มี methodupdateมาสังเกตการเปลี่ยนนั้น - คำถามที่ต้องถาม: child component เปิดพื้นผิว two-way ที่
bind:นี้สมมติไว้จริงหรือไม่ — และทุกที่ที่ชื่อ snippet ปรากฏ มันอยู่หลัง{@render}หรือเปล่า? - บทไหนอธิบายเรื่องนี้: Bindings พิสูจน์ error ของ
svelte-checkตรง ๆ เมื่อขาด$bindable(); Snippets & Render พิสูจน์ runtime errorinvalid_snippet_argumentsเมื่อเรียก snippet เปล่า ๆ; Actions พิสูจน์ว่า attachment รันซ้ำเมื่อ argument เปลี่ยน ในขณะที่use:action ไม่ทำแบบนั้นเงียบ ๆ
4. Event และ DOM
หัวข้อที่มีชื่อว่า “4. Event และ DOM”Event handler ของ Svelte 5 เป็นแค่ property ธรรมดา (onclick ไม่ใช่ on:click) และส่วนใหญ่ถูก delegate ไปยัง root listener เดียวแทนที่จะผูกทีละ element ทั้งสองรายละเอียดสำคัญมากตอนอ่านโค้ดที่ AI เขียนซึ่งไปแตะ DOM ตรง ๆ — stopPropagation() บน vanilla listener ระหว่าง root กับ Svelte handler จะทำลาย handler นั้นเงียบ ๆ และโค้ดที่สมมติว่า use: action “ทำงานเองอัตโนมัติ” เหมือน reactive prop พลาดจุดที่ว่ามีแค่ method update ที่เขียนไว้ชัดเจน (หรือในโค้ดใหม่ ใช้ attachment) เท่านั้นที่ทำให้เป็นจริง
- ต้องมองหาอะไร:
addEventListenerที่ผูกมือระหว่าง DOM root กับonclickของ Svelte โดยเฉพาะตัวที่เรียกstopPropagation();use:action ที่ parameter เปลี่ยนหลัง mount โดยไม่มี methodupdate; โค้ดที่ควรเป็น attachment ระดับ component แทน เพราะuse:ใช้ได้กับ element เท่านั้น - คำถามที่ต้องถาม: ถ้ามีอะไรบางอย่างระหว่าง root กับ element นี้หยุดการ bubble ของ native event, delegated handler ยังทำงานอยู่หรือไม่ — และโค้ด DOM เชิง imperative นี้รันซ้ำจริงเมื่อ input ของมันเปลี่ยนหรือรันแค่ครั้งเดียวตอน mount?
- บทไหนอธิบายเรื่องนี้: Events & Attributes พิสูจน์ event delegation และช่องโหว่
stopPropagation()ตรง ๆ ด้วย ancestor listener จริง; Actions พิสูจน์ความต่างของการรันซ้ำระหว่าง attachment กับuse:ด้วยเทสต์นับจำนวนครั้ง
5. ขอบเขตฝั่งเซิร์ฟเวอร์ของ SvelteKit
หัวข้อที่มีชื่อว่า “5. ขอบเขตฝั่งเซิร์ฟเวอร์ของ SvelteKit”โค้ดที่รันบนเซิร์ฟเวอร์มีมุมคมที่ไม่โผล่มาตอน review เฉพาะ component: redirect()/error() ทำงานด้วยการ throw ซึ่ง try/catch ที่เขียนกว้างเกินไปสามารถกลืนมันไปเงียบ ๆ; universal +page.js load รันในเบราว์เซอร์ด้วย จึงห้าม import server-only env หรือแตะฐานข้อมูลเด็ดขาด; และ form action ที่คืนค่า object เปล่าแทน fail() ดูถูกต้องใน UI แต่พก HTTP status ผิดอยู่ข้างใต้
- ต้องมองหาอะไร:
try/catchรอบโค้ดที่เรียกredirect()/error()โดยไม่ตรวจisRedirect/isHttpErrorก่อนถือว่า catch คือความล้มเหลวจริง;$env/static/private(หรือ server-only import ใด ๆ) ถูกอ้างจาก+page.jsแทนที่จะเป็น+page.server.js; form action ที่คืน{ error: '...' }แทนfail(status, { error: '...' });loadที่awaitข้อมูลที่การวาดครั้งแรกไม่ต้องใช้ แทนที่จะคืน promise ที่ยังไม่ await ให้{#await}stream;import { page } from '$app/stores'แทน$app/state - คำถามที่ต้องถาม: ถ้า code path นี้ throw
redirect()/error()จริง มีอะไรระหว่าง throw กับ handler ของ SvelteKit เองที่จะจับมันไปโดยไม่ตั้งใจหรือไม่ — และ server-only import ทุกตัวในไฟล์นี้อยู่ในไฟล์ที่ไม่มีวันถูกส่งไปเบราว์เซอร์จริงหรือไม่? - บทไหนอธิบายเรื่องนี้: Server & API พิสูจน์บั๊ก
redirect()ถูกtry/catchกลืนด้วย curl จริง พร้อมพื้นผิว hooks เต็มรูปแบบ; Forms & Actions พิสูจน์ช่องว่างของ HTTP status ระหว่างfail()กับ object เปล่า; Loading Data พิสูจน์ streaming และการเลิกใช้$app/storesแทนที่ด้วย$app/state
6. Keys, transitions และ boundaries
หัวข้อที่มีชื่อว่า “6. Keys, transitions และ boundaries”{#each} ที่ไม่มี key ไม่ throw — มันแค่ใช้ component instance เดิมซ้ำตาม ตำแหน่ง เงียบ ๆ ทำให้การลบรายการกลางลิสต์อาจเหลือแถวที่ไม่เกี่ยวข้องกันเลยถือ state ภายในผิดตัว <svelte:boundary> กับ async Svelte เพิ่มอีกชั้น: snippet pending ครอบคลุมแค่การวาดครั้งแรกของ boundary เท่านั้น และ await ในเทมเพลตเป็น compile error จริงถ้าไม่เปิด flag experimental ที่โค้ดที่ AI สร้างส่วนใหญ่ไม่เคยพูดถึง
- ต้องมองหาอะไร:
{#each items as item}ที่ไม่มี key(item.id)โดยเฉพาะเมื่อแต่ละแถวมี local state เป็นของตัวเอง (ปุ่ม expand/collapse, ฟอร์มร่าง);reset()ที่ถูกเรียกแบบ synchronous ภายใน callback<svelte:boundary onerror>;awaitที่ใช้ตรง ๆ ในเทมเพลตหรือ$derivedโดยไม่มีcompilerOptions.experimental.async: trueอยู่ที่ไหนเลยในโปรเจกต์ - คำถามที่ต้องถาม: ถ้าลิสต์นี้เรียงลำดับใหม่หรือย่อลง state ภายในของแต่ละแถวติดตามข้อมูลของมันไปด้วยหรือไม่ หรือมันติดอยู่กับตำแหน่งในลิสต์เงียบ ๆ — และถ้า component นี้ throw กลางการ render มี boundary ล้อมมันอยู่หรือเปล่า?
- บทไหนอธิบายเรื่องนี้: Async Svelte พิสูจน์ compile error
experimental_asyncและการแยกระหว่าง boundary กับ$effect.pending(); Lifecycle พิสูจน์ error จับเวลาsvelte_boundary_reset_onerror; AI Bug Catalog กลุ่มสุดท้ายพิสูจน์บั๊กการสลับ state ของ{#each}ที่ไม่มี key แบบครบวงจร
โมดูลนี้ตามด้วย AI Bug Catalog (28 รูปแบบ จัดกลุ่มตามลำดับนี้เป๊ะ), เช็คลิสต์หน้าเดียว Review Checklist, Verification Tools, และการ review แอป Svelte ที่ AI เขียนแบบเต็มรูปแบบสามครั้ง: Worked Review 1, Worked Review 2, และ Worked Review 3
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Reading order for a Svelte file you did not write:
1. Runes vs legacy mode -> is every reactivity primitive even legal in this file?2. State ownership -> does $state/$derived/$effect each do the job it is for?3. Props/bindings/snippets -> does bind: have the $bindable() it assumes, is {@render} used?4. Events and DOM -> does a stopPropagation() or a missing update() break this silently?5. SvelteKit boundary -> does a try/catch swallow a throw, does server-only code leak client-side?6. Keys, transitions, boundaries -> does list state follow the right row, is there a boundary at all?
Skipping straight to "does it pass svelte-check" answers almost none of these — most of thismodule's catalog compiles clean, and several entries produce no warning of any kind.ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ถ้า
svelte-checkผ่าน แปลว่าโค้ดถูกต้อง” entry ส่วนใหญ่ในแคตาล็อกของโมดูลนี้ compile และ type-check ผ่านสะอาดsvelte-checkพิสูจน์แค่ว่ารูปร่างถูกต้องและกฎแข็งของ compiler ถูกปฏิบัติตาม — ไม่เคยพิสูจน์ว่าพฤติกรรมตอนรันจริงตรงกับที่โค้ดรอบข้างสมมติไว้ - “ไฟล์หนึ่งเป็นได้แค่ ‘Svelte เก่า’ หรือ ‘Svelte ใหม่’ อย่างสม่ำเสมอตลอดทั้งไฟล์” พิสูจน์แล้วว่าผิดซ้ำแล้วซ้ำเล่าในโมดูลนี้: ไฟล์เดียวผสม compile error แข็ง (
export letคู่กับ rune) กับ warning เบา (on:click) กับรูปแบบที่ compile ผ่านเงียบ ๆ (createEventDispatcher) — การรู้ว่ารูปแบบเก่าแต่ละแบบอยู่กลุ่มไหนสำคัญกว่าการตัดสิน “เก่าหรือไม่” แบบเดียว - “Svelte ที่ AI เขียนต้องการความละเอียดมากกว่า Svelte ที่คนเขียน” ทุกรูปแบบในแคตาล็อกของโมดูลนี้ก็ปรากฏในโค้ดที่คนเขียนเช่นกัน — ผู้ช่วย AI แค่ผลิตมันซ้ำด้วยความมั่นใจสูงกว่า และกระจายไปหลายไฟล์กว่าที่คนมักทำ
- “ถ้า component แสดงผลถูกต้องครั้งเดียว การจัดการ state ของมันก็ถูกต้อง” บั๊กหลายตัวในโมดูลนี้ (การสลับ state ของ
{#each}ที่ไม่มี key, ความล่าช้าของ effect-mirror, การรั่วของ module-state ตอน SSR) โผล่มาแค่ตอน interaction ครั้งที่สอง, การเรียงลำดับใหม่, หรือคำขอที่เกิดพร้อมกันเท่านั้น — การ render ครั้งเดียวแทบไม่พิสูจน์อะไรเรื่องความเป็นเจ้าของเลย