The Component Model
Component คือ function ที่ render บน server
หัวข้อที่มีชื่อว่า “Component คือ function ที่ render บน server”Astro component คือไฟล์ .astro ที่คุณ import และใช้ในอีกไฟล์ได้ รับ props เข้ามาแล้วคืน HTML ออกไป เพราะ render บน server component จึงเป็น function จาก props ไปเป็น markup — ไม่มี lifecycle ฝั่ง client
---interface Props { title: string; href: string;}const { title, href } = Astro.props; // props arrive on Astro.props---<a class="card" href={href}> <h3>{title}</h3></a>การใช้งาน:
---import Card from '../components/Card.astro';---<Card title="Hello" href="/hello" />props เข้ามาผ่าน Astro.props และคุณกำหนด type ของ props ด้วยการประกาศ interface Props ใน frontmatter — Astro ใช้ interface นี้ type-check การใช้งาน ที่นี่ไม่มี useState component รันครั้งเดียวเพื่อผลิต HTML
ลืม prop ที่จำเป็นไปเป็น type error จริงที่เช็คได้ — ไม่ใช่เรื่องเซอร์ไพรส์ตอน runtime พิสูจน์ด้วย astro check จริงกับ Card component ตัวเดียวกันข้างบน เรียกโดยมีแค่ title:
---// @expect-error deliberately omits the required `href` prop, proven belowimport Card from '../components/Card.astro';---<Card title="Hello" />$ npx astro checksrc/pages/card-missing-prop.astro:4:2 - error ts(2322): Type '{ title: string; }' is not assignable to type 'IntrinsicAttributes & Props'. Property 'href' is missing in type '{ title: string; }' but required in type 'Props'.
<Card title="Hello" /> ~~~~นั่นคือประโยชน์เชิงปฏิบัติของการกำหนด type ให้ props เลย: prop ที่หายไป (หรือ type ผิด) ถูกจับได้ก่อนที่หน้าจะ render เลยด้วยซ้ำ ตรงจุดที่เรียกใช้เป๊ะ ๆ
พิสูจน์: component เป็น pure function ของ props จริง ๆ
หัวข้อที่มีชื่อว่า “พิสูจน์: component เป็น pure function ของ props จริง ๆ”ถ้า component เป็น function จาก props ไปเป็น HTML จริง ๆ โดยไม่มี state ซ่อนอยู่ การเรียกมันสองครั้งด้วย props เดียวกันต้องได้ output เดียวกันเป๊ะ — ไม่ต้องมี client ไม่ต้องมี browser แค่ render เฉย ๆ Astro Container API (experimental_AstroContainer พูดถึงเต็ม ๆ ในบท testing) render component เป็น string แบบแยกตัว ทำให้เทสแบบนี้ทำได้ตรง ๆ:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';import { expect, test } from 'vitest';import Card from '../components/Card.astro';
test('Card is a pure function of props: same props, same output', async () => { const container = await AstroContainer.create(); const a = await container.renderToString(Card, { props: { title: 'Hello', href: '/hello' } }); const b = await container.renderToString(Card, { props: { title: 'Hello', href: '/hello' } }); expect(a).toBe(b); expect(a).toContain('Hello'); expect(a).toContain('href="/hello"');});$ npx vitest run src/__tests__/card.test.ts ✓ src/__tests__/card.test.ts (1 test) 1 passedกำหนด type ให้ props อย่างละเอียด: HTMLAttributes, Polymorphic, ComponentProps
หัวข้อที่มีชื่อว่า “กำหนด type ให้ props อย่างละเอียด: HTMLAttributes, Polymorphic, ComponentProps”นอกเหนือจาก interface Props ที่เขียนเอง astro/types export helper สามตัวสำหรับ pattern ของ props ที่เจอบ่อยมาก — แต่ละตัว verify ตรง ๆ จาก types.d.ts ของ [email protected] ที่ติดตั้งจริง:
HTMLAttributes<Tag>— attribute จริงในตัวของ tag HTML นั้น ๆ ทำให้ wrapper component รับทุกอย่างที่ element จริงรับได้ (onclick,aria-*,data-*, …) โดยไม่ต้องประกาศเองใหม่ทีละตัว:
---import type { HTMLAttributes } from 'astro/types';
interface Props extends HTMLAttributes<'button'> { variant?: 'primary' | 'secondary';}const { variant = 'primary', ...attrs } = Astro.props;---<button class={`btn btn-${variant}`} {...attrs}> <slot /></button>Polymorphic<P>— กำหนด type ให้ component ที่ render เป็น tag อื่นได้ผ่าน propas(<Heading as="h2">ที่ต้องการ attribute จริงของh2ไม่ใช่ element ตายตัว):
---import type { HTMLTag, Polymorphic } from 'astro/types';
type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }>;const { as: Tag = 'h2' as const, ...attrs } = Astro.props;---<Tag {...attrs}><slot /></Tag>ComponentProps<T>— ดึง type ของ props จาก function component (framework island) มีประโยชน์เวลาอยากรับ “props อะไรก็ตามที่SomeIslandรับได้” โดยไม่ต้อง import แล้วเขียน interfacePropsซ้ำ:
import type { ComponentProps } from 'astro/types';import type Counter from '../components/Counter.tsx';
type CounterProps = ComponentProps<typeof Counter>;flowchart LR
a["HTMLAttributes<'button'>"] --> u1["สืบ attribute ของ element จริง"]
b["Polymorphic<{ as: Tag }>"] --> u2["กำหนด type ให้ component แบบ as=..."]
c["ComponentProps<typeof Fn>"] --> u3["reuse prop type ของ function component"]Slots: composition ด้วยการส่ง markup
หัวข้อที่มีชื่อว่า “Slots: composition ด้วยการส่ง markup”บ่อยครั้ง component ต้องห่อ content ที่ไม่รู้ล่วงหน้า นั่นคือหน้าที่ของ slots — markup ลูกที่คุณวางไว้ระหว่าง tag ของ component จะถูก render ตรงที่ <slot /> ปรากฏ
---const { heading } = Astro.props;---<section class="panel"> <h2>{heading}</h2> <slot /> <!-- children go here --></section><Panel heading="Notes"> <p>Any markup here lands in the slot.</p></Panel>ถ้าต้องการจุดแทรกมากกว่าหนึ่งจุด ใช้ named slots:
<header><slot name="header" /></header><main><slot /></main> <!-- the default slot --><footer><slot name="footer" /></footer><Layout> <h1 slot="header">Title</h1> <p>Body goes in the default slot.</p> <small slot="footer">© 2026</small></Layout>flowchart LR parent["Parent ส่ง markup ระหว่าง tag"] --> slot["component render มัน ที่ <slot />"] slot --> out["HTML ที่ประกอบแล้ว"]
Slots คือกระดูกสันหลังของ layouts: Layout.astro นิยามเชลล์ของหน้า (head, nav, footer) พร้อม <slot /> สำหรับ body ของหน้า และทุกหน้าก็ห่อ content ของตัวเองไว้ใน layout นั้น
Scoped styles
หัวข้อที่มีชื่อว่า “Scoped styles”tag <style> ภายใน .astro component เป็น scoped ให้ component นั้นโดย default — Astro เขียน selector ใหม่เพื่อไม่ให้ style รั่วออกไปชนกับ component อื่น
<h1>Title</h1><style> /* This h1 rule applies ONLY to this component's h1, not others on the page. */ h1 { color: rebeccapurple; }</style>คุณได้ CSS แบบ component-local ด้วย selector ธรรมดา — ไม่มี CSS-in-JS ไม่ต้องมีพิธีตั้งชื่อแบบ BEM เมื่อคุณ อยาก ให้ rule ใช้แบบ global คุณ opt out ด้วย <style is:global> (พูดถึงในบท styles)
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”.astro component snippet ที่ AI generate มา มักพกสมมติฐานจาก client framework มาด้วย เช็คสิ่งเหล่านี้:
- มันเรียก
useState,useEffect, หรือ hook อื่นที่ top level ของ frontmatter หรือเปล่า? ไม่มีอะไรแบบนั้นอยู่ใน script ฝั่ง server-only ของ component.astroเลย — hook พวกนั้นอยู่ใน framework component ที่ hydrate แล้ว (island) เท่านั้น ไม่ใช่ไฟล์.astro - มันประกาศ
Propsแต่ template destructure field ที่ไม่เคยอยู่ใน interface หรือเปล่า?astro checkจับ field ที่จำเป็น หายไป ได้ (พิสูจน์แล้วข้างบนด้วยCard) — แต่ field แบบ optional ที่พิมพ์ผิดใน template (hrafแทนhref) จะอ่านundefinedเงียบ ๆ ที่ระดับ template เว้นแต่ field นั้นจะถูกอ้างชื่อผิดในAstro.propsด้วย - component แบบ
Polymorphic/propashard-code tag ที่ render ไว้ในตัว logic ของมันเองที่ไหนหรือเปล่า (เช่น เช็คas === 'button'เพื่อตัดสินใจอะไรบางอย่าง) ซึ่งขัดกับจุดประสงค์ของการทำให้ polymorphic? - มันใช้
<slot />ใน component ที่ render เป็น framework island (เช่นclient:load) หรือเปล่า? named/default slot เป็น feature ของ Astro template — “สิ่งที่เทียบเท่า slot” ของ framework component คือchildren/named props ไม่ใช่<slot /> - block
<style>แบบ scoped ถูกสันนิษฐานว่ามีผลกับ markup ของ component ลูก หรือเปล่า (เช่น style<h3>ภายในของ<Card>จาก parent)? scoped style มีผลแค่กับ selector ที่ match element ใน ไฟล์เดียวกัน เท่านั้น การเอื้อมเข้าไปใน child ต้องใช้:global()หรือ class ที่ขับเคลื่อนด้วย prop
🐛 หา bug ในโค้ดนี้
---interface Props {name: string;role?: 'admin' | 'member';}const { name, role = 'member' } = Astro.props;---<span class={`badge badge-${role}`}>{nam}</span>เฉลย
template อ่าน {nam} — พิมพ์ผิดจาก {name} — และมันเป็น astro check error จริง แต่ไม่ใช่ error แบบ props-shape ที่คาดไว้จากกรณี “prop หายไป” ข้างบน พิสูจน์กับ snippet นี้เป๊ะ ๆ:
src/pages/userbadge-test.astro:8:38 - error ts(2552): Cannot find name 'nam'. Did you mean 'name'?
<span class={`badge badge-${role}`}>{nam}</span> ~~~nam ไม่ใช่ identifier ที่ถูกประกาศไว้เลย TypeScript จึง report ts(2552) (“cannot find name” พร้อมคำแนะนำ “did you mean”) — ไม่ใช่ ts(2322) แบบ interface Props ไม่ตรงกันเหมือนข้างบน เป็น bug คนละประเภทจากกรณี prop ที่จำเป็นหายไป: อันนั้นคือ props ผิด ส่วนอันนี้คือพิมพ์ผิดใน template ที่อ้างถึงสิ่งที่ไม่เคยถูก destructure ทั้งคู่เป็น astro check error จริง แต่คนละเหตุผล — นี่แหละคือเหตุผลที่ควรอ่านให้ลึกกว่า “astro check ผ่านไหม” ไปถึง “astro check ผ่าน ด้วยเหตุผลที่คาดไว้ ไหม”
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”คิดถึง .astro component เหมือน pure server-side template function: render(props) -> html props ไหลเข้ามาทิศทางเดียว ผ่าน Astro.props ไม่มีอะไรไหลกลับออกไปนอกจาก markup ที่ render แล้ว Slots ก็แค่ input อีกแบบหนึ่ง — markup แทนที่จะเป็นข้อมูล — ส่งแบบตำแหน่ง (default) หรือแบบชื่อ ไม่มี lifecycle ให้คิดเพราะไม่มี “ทีหลัง”: function รันครั้งเดียว คืน HTML แล้วจบ Container API test ข้างบนคือหลักฐานตรง ๆ: input เดียวกัน output เดียวกัน ทุกครั้ง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”.astro component |
Framework island (.tsx/.vue/…) |
|
|---|---|---|
| รันที่ไหน | server เท่านั้น (function ของ props ไม่มี lifecycle) | server (render แรก) และ client (ถ้า hydrate) |
| State | ไม่มี — re-run ใหม่จากศูนย์ทุกครั้งที่ render | useState/reactive state คงอยู่ข้าม re-render |
| Slots | <slot /> / named slots |
children / props ตาม convention ของ framework |
| Styling | <style> แบบ scoped มีในตัว, selector ธรรมดา |
convention ของ framework เอง (CSS modules, styled-components, …) |
| กำหนด type ให้ props | interface Props หรือ HTMLAttributes/Polymorphic/ComponentProps จาก astro/types |
convention การกำหนด type props ของ framework เอง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “prop ที่จำเป็นหายไปจะเป็น
undefinedตอน runtime จับได้ (ถ้าจับได้) ตอนหน้าพัง” พิสูจน์แล้วข้างบน:astro checkจับได้ตรงจุดที่เรียกใช้เป๊ะ ๆ ก่อนที่หน้าจะ render เลยด้วยซ้ำ —ts(2322), “Property ‘href’ is missing” - “
.astrocomponent เก็บ state ข้าม render ได้ เหมือน class ที่มี instance field” เก็บไม่ได้ — พิสูจน์ด้วย Container API test ข้างบน: props เดียวกันได้ output เหมือนกันทุก byte เสมอ เพราะ component ถูก re-run ใหม่จากศูนย์ทุกครั้ง ไม่มีอะไรคงอยู่เลย - “
HTMLAttributes<'button'>ก็แค่{ [key: string]: any }” มันคือ attribute set จริงและเฉพาะเจาะจงของ tag นั้น (verify ตรง ๆ จากastro/types.d.ts) — ส่ง attribute ที่ไม่มีจริงบน<button>(เช่นhrefตรง ๆ) ก็ยังเป็น type error อยู่ดี