Snippets & Render
Snippets: ก้อน markup ที่ใช้ซ้ำได้
หัวข้อที่มีชื่อว่า “Snippets: ก้อน markup ที่ใช้ซ้ำได้”snippet คือ block ของ markup ที่มีชื่อ ที่คุณ render ได้หลายครั้ง นิยามด้วย {#snippet name(params)}…{/snippet} และ render ด้วย {@render name(args)} เป็นวิธีของ Svelte ที่ทำให้ template DRY
<script> let items = $state([ { name: 'Coffee', price: 3 }, { name: 'Tea', price: 2 }, ]);</script>
{#snippet row(item)} <tr><td>{item.name}</td><td>${item.price}</td></tr>{/snippet}
<table> {#each items as item (item.name)} {@render row(item)} {/each}</table>snippet row นิยามครั้งเดียวแล้ว render ต่อ item snippet รับ parameter เหมือน function ดังนั้น markup เดียวกันปรับตาม data ต่างกันได้
flowchart LR
def["{#snippet row(item)} ... {/snippet}"] --> render1["{@render row(a)}"]
def --> render2["{@render row(b)}"]
render1 --> out["markup instances"]
render2 --> out Snippet parameter รวมถึงแบบ optional
หัวข้อที่มีชื่อว่า “Snippet parameter รวมถึงแบบ optional”parameter ของ snippet ทำงานเหมือน function เป๊ะ ๆ — รวมถึง default value สำหรับ parameter ที่ผู้เรียกอาจไม่ส่งมา:
<script lang="ts"> let names: (string | undefined)[] = ['Ada', undefined];</script>{#snippet greeting(name = 'friend')} <p data-testid="g">Hello, {name}!</p>{/snippet}{#each names as n} {@render greeting(n)}{/each}import { test, expect } from 'vitest';import { render } from '@testing-library/svelte';import OptionalParam from './OptionalParam.svelte';
test('a snippet parameter can have a default value, used when the argument is undefined', () => { const { container } = render(OptionalParam); const texts = [...container.querySelectorAll('[data-testid="g"]')].map((el) => el.textContent); expect(texts).toEqual(['Hello, Ada!', 'Hello, friend!']);});✓ a snippet parameter can have a default value, used when the argument is undefinedgreeting(n) ที่ n === undefined fallback ไปที่ 'friend' เหมือน default parameter ของ JS เป๊ะ ๆ — เพราะมัน compile ไปเป็นแบบนั้นจริง ๆ
ลืม {@render}: error จริงตอน runtime
หัวข้อที่มีชื่อว่า “ลืม {@render}: error จริงตอน runtime”การเขียนชื่อ snippet เป็น call ธรรมดา — {row(item)} — แทน {@render row(item)} compile ผ่านโดยไม่มี error หรือ warning เลย แต่ throw ตอน runtime ทันทีที่บรรทัดนั้นถูก execute จริง:
<script lang="ts"> let items = [1, 2, 3];</script>{#snippet row(item: number)} <li data-testid="row">{item}</li>{/snippet}<ul> {#each items as item} {row(item)} {/each}</ul>import { test, expect } from 'vitest';import { render } from '@testing-library/svelte';import NoRender from './NoRender.svelte';
test('calling a snippet without {@render} throws at runtime', () => { expect(() => render(NoRender)).toThrowError(/invalid_snippet_arguments/);});✓ calling a snippet without {@render} throws at runtimeerror จริง: invalid_snippet_arguments — “A snippet function was passed invalid arguments. Snippets should only be instantiated via {@render ...}” (svelte.dev/e/invalid_snippet_arguments) ภายใต้ผิว snippet compile ไปเป็น function ที่ parameter ตัวแรกเป็น DOM anchor ภายในที่ {@render} เป็นคนใส่ให้ — การเรียกตรง ๆ ทำให้ argument ของคุณเอง (item ซึ่งเป็น array index) เข้าไปแทนที่ตำแหน่งนั้น ซึ่งเป็นสิ่งที่ทำให้ check นี้ทำงาน เพราะทั้ง compiler และ svelte-check ไม่จับสิ่งนี้เลย (มันคือความผิดพลาดเชิงรูปแบบ template ไม่ใช่ type error) นี่คือสิ่งที่ควรเช็คตอนอ่านโค้ดที่ generate มา — ดูด้านล่าง
Snippets แทนที่ slot
หัวข้อที่มีชื่อว่า “Snippets แทนที่ slot”ใน Svelte 4 คุณส่ง markup เข้า component ด้วย <slot> (และ named slot ผ่าน <slot name="x">) Svelte 5 แทนที่สิ่งนั้นทั้งหมดด้วย snippet — มันทรงพลังกว่าเพราะรับ parameter ได้และเป็นแค่ value ที่ส่งต่อกันได้
<!-- Svelte 4 (old): a component defined <slot /> and <slot name="header" /> --><!-- Svelte 5 (new): a component renders {@render children()} and {@render header()} -->ถ้าคุณอ่าน tutorial เก่าที่ใช้ <slot> สิ่งที่เทียบเท่าในยุคใหม่คือ snippet ที่ render ด้วย {@render} บทถัดไปจะพูดถึงว่า parent ส่ง snippet (รวมถึง snippet พิเศษชื่อ children) เข้า child อย่างไรเป๊ะ ๆ
<slot> ยัง compile และทำงานได้ ใน component ที่เป็น runes-mode ทุกวันนี้ (Svelte 5.57.1) — มัน backward-compatible ไม่ได้ถูกถอดออก — แต่ compiler จะ flag มันด้วย deprecation warning จริง:
Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags insteadhttps://svelte.dev/e/slot_element_deprecatedSnippets เป็น value
หัวข้อที่มีชื่อว่า “Snippets เป็น value”เพราะ snippet เป็น value คุณส่งมันเข้า snippet อีกตัว, เก็บไว้ในตัวแปร, หรือเลือกระหว่าง snippet แบบมีเงื่อนไขได้:
{#snippet loading()}<p>Loading…</p>{/snippet}{#snippet ready(data)}<p>{data}</p>{/snippet}
{#if isLoading} {@render loading()}{:else} {@render ready(result)}{/if}ความ composable นี้ — snippet เป็น first-class parameterized value — คือเหตุผลที่มันแทนที่ slot mechanism ที่แข็งกว่านี้
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”snippet คือ function ที่ render markup — {#snippet name(params)} คือการ declare, {@render name(args)} คือการเรียก ทุกอย่างเกี่ยวกับ function ธรรมดาใช้ได้หมด: parameter, default, closure เหนือตัวแปรภายนอก, ส่งต่อเป็น value สิ่งเดียวที่ ไม่ เหมือน function ธรรมดาคือการเรียกตรง ๆ ({row(item)}) — framework ไม่ใช่โค้ดคุณ เป็นคนใส่ argument ตัวแรกจริง (DOM anchor) เมื่อคุณผ่าน {@render} ดังนั้นการข้าม {@render} ทำให้ value ของคุณเองไปแทนที่ตำแหน่งที่ตั้งใจไว้สำหรับ internal ของ Svelte
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ได้อะไร | ประโยชน์ | ต้นทุน |
|---|---|---|
Snippets แทน <slot> | parameter, first-class value, composable (พิสูจน์ใน Snippets เป็น value) | ต้องเรียนรู้ concept เพิ่มถ้ามาจาก tutorial Svelte 4 |
| default parameter ของ snippet | ผู้เรียกละ argument ได้สะอาด ๆ พิสูจน์แล้วด้านบน | ลืมง่ายว่า default ใช้เฉพาะตอน undefined ไม่ใช่ null/0/'' |
<slot> backward-compat | โค้ดเก่าและ tutorial เก่ายังทำงานได้โดยไม่ต้องแก้ | deprecated (warning จริง พิสูจน์แล้วด้านบน) — ผสมเข้าโค้ด runes-mode ใหม่ดูล้าสมัย |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ลืม
{@render}แล้วได้ compile error เหมือนพิมพ์ type ผิด” ไม่จริง — พิสูจน์แล้วด้านบน{row(item)}compile ผ่านสะอาด และจะ fail ก็ต่อเมื่อบรรทัด template นั้น run จริง ด้วยinvalid_snippet_arguments - “
<slot>ใน component Svelte 5 คือ error หนัก” ไม่จริง — พิสูจน์แล้วด้านบนว่ายัง compile และ render ถูกต้อง มันเป็นแค่ deprecation warning ไม่ใช่การถอดออก - “snippet ที่มี default parameter ทำงานเหมือน default ของ component prop” แนวคิดคล้ายกันแต่กลไกต่าง — default ของ snippet เป็น semantics ของ default parameter แบบ JS ธรรมดา ที่ evaluate ตอน
{@render}ไม่ใช่ destructuring ของ$props()
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลารีวิวโค้ด snippet ที่ generate มา ให้เช็ค:
- มี
{#snippet ...}ที่ถูกอ้างเป็น{name(args)}ที่ไหนแทนที่จะเป็น{@render name(args)}ไหม? มัน compile ผ่านเงียบ ๆ และ fail ตอน runtime เท่านั้น พิสูจน์แล้วด้านบน — grep หาชื่อ snippet แล้วยืนยันว่าทุกจุดที่ใช้ผ่าน{@render} - มี
<slot>ใช้ข้างใน component ที่ใช้ runes อยู่แล้ว ($state,$props()ฯลฯ) ไหม? มันยังทำงานได้ (พิสูจน์แล้วด้านบน) แต่การผสม slot ของ Svelte 4 เข้ากับ runes ของ Svelte 5 ในไฟล์เดียวกันไม่สม่ำเสมอ และมี deprecation warning จริง — ควรเป็นchildren/named snippet prop แทน - default parameter ของ snippet ถูกสมมติว่าครอบคลุมค่า “falsy” ของผู้เรียก (
0,'',false) ทั้งที่จริงมัน trigger เฉพาะตอนundefinedเท่านั้นไหม? - มี snippet ที่นิยามไว้แต่ไม่เคย render ที่ไหนเลยไหม (markup ตาย หรือพิมพ์ชื่อผิดตอนส่งให้
{@render})?
🐛 หา bug ในโค้ดนี้
<script>let count = $state(0);</script>
<div>{count}</div>
<slot />
<button onclick={() => count++}>+1</button>เฉลย
component นี้ผสม runes ($state) เข้ากับ element <slot> ของ Svelte 4 มันยัง compile และ render เนื้อหาของ parent ได้ถูกต้อง — <slot> backward-compatible ไม่ได้ถูกถอดออก — แต่ compiler จะออก deprecation warning จริง: Using <slot> to render parent content is deprecated. Use {@render ...} tags instead (พิสูจน์แล้วด้านบน) โค้ดที่ generate มาผสมยุคแบบนี้เป็นสัญญาณว่าโมเดลถูก train ด้วยตัวอย่างผสมทั้ง Svelte 4 และ 5 วิธีแก้: ให้ component มี prop children ผ่าน $props() แล้ว render ด้วย {@render children()} ให้ตรงกับ style runes ของไฟล์ที่เหลือ