ข้ามไปยังเนื้อหา

Snippets & Render

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
นิยามครั้งเดียว render ได้หลายครั้ง

parameter ของ snippet ทำงานเหมือน function เป๊ะ ๆ — รวมถึง default value สำหรับ parameter ที่ผู้เรียกอาจไม่ส่งมา:

src/lib/OptionalParam.svelte
<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}
src/lib/optionalparam.test.ts
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 undefined

greeting(n) ที่ n === undefined fallback ไปที่ 'friend' เหมือน default parameter ของ JS เป๊ะ ๆ — เพราะมัน compile ไปเป็นแบบนั้นจริง ๆ

การเขียนชื่อ snippet เป็น call ธรรมดา — {row(item)} — แทน {@render row(item)} compile ผ่านโดยไม่มี error หรือ warning เลย แต่ throw ตอน runtime ทันทีที่บรรทัดนั้นถูก execute จริง:

src/lib/NoRender.svelte
<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>
src/lib/norender.test.ts
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 runtime

error จริง: 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 มา — ดูด้านล่าง

ใน 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 instead
https://svelte.dev/e/slot_element_deprecated

เพราะ 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 ที่แข็งกว่านี้

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()

เวลารีวิวโค้ด 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 ของไฟล์ที่เหลือ

นิยามและ render snippet อย่างไร?
snippet แทนที่อะไรใน Svelte 5?
เกิดอะไรขึ้นตอนบรรทัดนี้ run?
{#snippet row(item)}
  <li>{item}</li>
{/snippet}
{row(1)}
parameter ของ snippet มี default value ได้ไหม?
อะไรคือความจริงเกี่ยวกับ <slot> ใน component Svelte 5 ที่เป็น runes-mode?
<script>
  let count = $state(0);
</script>
<slot />