Framework Integrations
เอา framework ที่คุณถนัดมาใช้
หัวข้อที่มีชื่อว่า “เอา framework ที่คุณถนัดมาใช้”Astro ไม่มี client-side component runtime ของตัวเอง — แต่ให้คุณใช้ UI framework ที่คุณรู้จักอยู่แล้วกับส่วนที่ต้อง interactive แทน ทั้ง React, Vue, Svelte, Solid และ Preact ใช้เป็น island ได้หมด ผ่าน integration อย่างเป็นทางการ
เพิ่ม integration ด้วย CLI ซึ่งจะติดตั้ง package และตั้งค่า config ให้:
npx astro add reactคำสั่งนั้นอัปเดต astro.config.mjs ให้เอง:
import { defineConfig } from 'astro/config';import react from '@astrojs/react';
export default defineConfig({ integrations: [react()],});ตอนนี้ component .jsx สามารถ import เข้ามาในไฟล์ .astro และ hydrate ด้วย client directive ได้แล้ว
import framework component มาเป็น island
หัวข้อที่มีชื่อว่า “import framework component มาเป็น island”island เขียนด้วยสไตล์ปกติของ framework นั้น ส่วนไฟล์ .astro เป็นตัว import และตัดสินใจว่า hydrate เมื่อไหร่
// src/components/Counter.jsx — a plain React componentimport { useState } from 'react';
export default function Counter({ start }) { const [count, setCount] = useState(start); return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;}---import Counter from '../components/Counter.jsx'; // 1. import the framework component---<h1>Welcome</h1><Counter start={0} client:visible /> <!-- 2. hydrate it as an island -->ถ้าไม่มี client:visible เจ้า Counter นั้นจะ render เป็น static HTML (มีปุ่ม แต่ onClick ไม่ทำงาน) directive คือสิ่งที่ส่ง JavaScript ของตัวเอง
ผสม framework หลายตัวในหน้าเดียว
หัวข้อที่มีชื่อว่า “ผสม framework หลายตัวในหน้าเดียว”เพราะแต่ละ island เป็นอิสระต่อกัน คุณจึงใช้ framework คนละตัวในหน้าเดียวกันได้ — widget React อยู่ข้าง ๆ widget Svelte — ขอแค่ติดตั้ง integration ทั้งสองไว้ อันนี้มีประโยชน์จริงตอน migrate ทีละส่วน หรือตอนหยิบ component ที่มีเฉพาะใน ecosystem เดียวมาใช้
flowchart TB page["Astro page (static HTML)"] page --> r["React island — client:load"] page --> s["Svelte island — client:visible"] page --> v["Vue island — client:idle"]
JSX framework สองตัว นามสกุลไฟล์เดียวกัน: include/exclude
หัวข้อที่มีชื่อว่า “JSX framework สองตัว นามสกุลไฟล์เดียวกัน: include/exclude”Preact กับ React render .jsx/.tsx ทั้งคู่ ซึ่งกำกวมทันทีที่ติดตั้งทั้งสอง integration พร้อมกัน — ไซต์ของคอร์สนี้เองใช้ Preact แต่โปรเจกต์จริงบางครั้งต้องการ component ที่เป็น React เท่านั้นอยู่ด้วย ติดตั้งทั้งคู่โดยไม่ config อะไรเลยจะได้ warning จริงทุก build:
[@astrojs/preact] More than one JSX renderer is enabled. This will lead to unexpectedbehavior unless you set the include or exclude option. Seehttps://docs.astro.build/en/guides/integrations-guide/preact/#combining-multiple-jsx-frameworksทางแก้คือ config จริงที่ทำงานได้ ให้แต่ละ integration มีชุดไฟล์ที่ไม่ทับกัน — พิสูจน์ด้วยการ build astro.config.mjs นี้จริง กับโปรเจกต์ที่มีทั้ง island .tsx (Preact) และ .jsx (React):
import { defineConfig } from 'astro/config';import node from '@astrojs/node';import preact from '@astrojs/preact';import react from '@astrojs/react';
export default defineConfig({ adapter: node({ mode: 'standalone' }), integrations: [ preact({ include: ['**/*.tsx'] }), react({ include: ['**/*.jsx'] }), ],});พอแยกแบบนี้ ไฟล์ .jsx เป็นของ React แน่นอน ไฟล์ .tsx เป็นของ Preact แน่นอน — warning หายไป และ HTML ที่ build ออกมาพิสูจน์ว่าแต่ละตัว hydrate ด้วย renderer ของตัวเอง (chunk renderer-url คนละไฟล์ และ attribute prefix="r1" ที่มีแค่ใน island ของ React ไม่มีใน island ของ Preact):
<!-- .tsx (Preact) --><astro-island uid="Z22rHo8" data-preact-island-id="1" component-url="/_astro/Counter.CzecfPIK.js" renderer-url="/_astro/client.N277o4ni.js" ... client="visible" ...>
<!-- .jsx (React) --><astro-island uid="2lYo4w" prefix="r1" component-url="/_astro/RefDemo.OsmBpWw5.js" renderer-url="/_astro/client.CLhIxG29.js" ... client="load" ...>ธรรมเนียมของคอร์สนี้เอง: ใช้ .tsx เป็น default สำหรับ island ที่เป็น Preact — ตรงกับที่ไซต์ที่เหลือใช้อยู่แล้ว — แล้วใช้ island .jsx/React จริง เฉพาะตอนที่บทเรียนต้องการโชว์ React โดยเฉพาะ ให้ตรงกับการแยก include ข้างบน
props ต้อง serializable
หัวข้อที่มีชื่อว่า “props ต้อง serializable”เมื่อคุณส่ง props จากไฟล์ .astro เข้าไปใน island จะถูก render บน server แล้ว serialize ลงใน HTML เพื่อให้ client rehydrate ด้วยค่าเดิมได้ นั่นตั้งกฎเหล็กไว้ข้อหนึ่ง: props ของ island ต้อง serializable — string, number, boolean, array, plain object สิ่งที่ ส่งไม่ได้:
- function (รวมถึง event handler) เพราะไม่มีทาง serialize function ข้ามเส้น server/client
- class instance,
Dateobject ในบางกรณี และค่าที่ไม่ใช่ plain value อื่น ๆ
---import Chart from '../components/Chart.jsx';const data = await getMetrics(); // fetched on the server---<Chart data={data} client:visible /> <!-- fine: data is a plain array/object --><!-- <Chart onPoint={(p) => ...} /> — NOT allowed: functions cannot be serialized -->ถ้าคุณต้องการ behavior แทนที่จะเป็นข้อมูล ให้เอา logic นั้นไปไว้ ข้างใน island หรือส่ง children ที่ render บน server เข้าไปผ่าน slot (บทหน้า) มุมมองที่ช่วยให้ไม่พลาด: ไฟล์ .astro ส่ง ข้อมูล ให้ island ไม่ใช่ callback
React 19 feature ข้างใน island
หัวข้อที่มีชื่อว่า “React 19 feature ข้างใน island”@astrojs/react ของไซต์นี้ติดตั้ง React 19.3.0 เพราะ island ของ React hydrate เข้าไปเป็น React tree ธรรมดาที่เป็นอิสระ feature ส่วนใหญ่ของ React 19 เลยทำงานได้เหมือนอยู่ในแอป React ธรรมดา — พิสูจน์ด้วยสองตัวที่เป็นไฮไลต์
ref เป็น prop ธรรมดา (ไม่ต้องใช้ forwardRef แล้วตั้งแต่ React 19) ทำงานได้ข้างใน island รวมถึงส่งต่อเข้าไปใน function component ธรรมดาอีกชั้นหนึ่งด้วย:
// ReactField.jsx — React-only: a plain function component receiving `ref` as a normal propexport default function ReactField({ ref }) { return <input ref={ref} data-testid="react-field" defaultValue="" />;}import { useRef } from 'react';import ReactField from './ReactField.jsx';
export default function RefDemo() { const inputRef = useRef(null); return ( <div> <ReactField ref={inputRef} /> <button data-testid="focus-btn" onClick={() => inputRef.current?.focus()}> Focus the field </button> </div> );}build เป็น island แบบ client:load แล้วขับด้วย browser จริง — คลิกปุ่มจริงย้าย focus เข้าไปที่ input ที่อยู่ลึกลงไปอีกสอง component โดยไม่มี forwardRef ที่ไหนเลย:
document.activeElement.getAttribute('data-testid')// "react-field"useActionState ก็ทำงานได้ ในฐานะ React state ฝั่ง client ธรรมดา — ไม่มีสะพานไปหา Astro Actions หรือ server ที่ไหนเลย function ที่ส่งให้ก็แค่รันใน browser:
import { useActionState } from 'react';
async function increment(prevState, formData) { await new Promise((r) => setTimeout(r, 10)); return prevState + Number(formData.get('step') || 1);}
export default function ActionForm() { const [state, formAction, isPending] = useActionState(increment, 0); return ( <form action={formAction}> <input type="hidden" name="step" value="5" /> <button type="submit" data-testid="action-btn"> {isPending ? 'Adding...' : `Total: ${state}`} </button> </form> );}render บน server เป็น Total: 0; คลิกจริง submit form แล้ว state อัปเดตฝั่ง client ล้วน ๆ:
document.querySelector('[data-testid=action-btn]').textContent// before click: "Total: 0"// after click: "Total: 5"สิ่งที่ไม่ข้าม boundary ของ island แม้จะเป็น React 19: React Server Component หรือ action แบบ 'use server' ไม่มีความหมายที่นี่เลย — island ของ Astro คือ client component tree ธรรมดา ไม่ใช่ RSC runtime ของ React ทำให้ function action ของ useActionState เป็นแค่ async function ฝั่ง client ธรรมดา (พิสูจน์ข้างบน: ไม่มี network request เกิดขึ้นเลย รันใน browser ทั้งหมด) และทุกอย่างจากบทอื่นยังใช้ได้ข้างใน island React 19 เหมือนที่อื่น: prop ที่ข้ามจาก .astro เข้าไปใน island ยังต้อง serializable (พิสูจน์ข้างบน) และ Context.Provider ก็ยังไปไม่ถึง island พี่น้อง (พิสูจน์ในบท sharing-state) — React 19 เปลี่ยนแค่สิ่งที่เขียนได้ข้างใน tree ของ island ตัวเอง ไม่ได้เปลี่ยนสิ่งที่ข้ามระหว่าง island
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอนเจอ snippet ที่ generate มาผสม framework หรือใช้ framework feature ใหม่ ๆ ข้างใน island เช็คสิ่งเหล่านี้ก่อนเชื่อ:
- มี JSX-capable integration สองตัว (Preact + React, Preact + Solid…) ติดตั้งโดยไม่มี
include/excludeไหม? พิสูจน์ข้างบนแล้วว่านั่นคือ warning จริงทุก build (More than one JSX renderer is enabled) ไม่ใช่แค่สมมติฐาน - fence
.jsxอ้างว่าเป็น “เวอร์ชัน React” ของ component ทั้งที่preact()integration ของโปรเจกต์ไม่มีexcludeให้มันไหม? ถ้าไม่มีinclude/excludeที่แยกกันชัดเจน renderer ตัวไหนจะได้ไฟล์นั้นจริง ๆ ขึ้นอยู่กับลำดับการลงทะเบียน integration ไม่ใช่ syntax ของ framework ที่โค้ดใช้ - prop ที่เป็น class หรือ function ถูกส่งจาก
.astroเข้าไปใน island ของ framework ไหนก็ตามไหม? กฎ serializable-props (พิสูจน์ข้างบน) ใช้เหมือนกันหมดกับ island ของ React, Preact, Vue, Svelte และ Solid — เป็นคุณสมบัติของ boundary ของ island เอง ไม่ใช่ของ framework ตัวไหนตัวหนึ่ง - snippet สมมติว่า pattern แบบ React Server Component (
'use server', API เฉพาะ RSC) ใช้ได้ข้างใน island ของ Astro ไหม? island คือ client component tree ธรรมดาของ framework ที่ render มัน — ไม่มี RSC runtime อยู่ข้างใต้เลย พิสูจน์ข้างบนแล้วว่า action ของuseActionStateรันเป็น client function ธรรมดา ไม่ใช่ round-trip ไป server npx astro add <framework>ถูกข้ามไปแล้วแก้astro.config.mjsกับpackage.jsonเองไหม? ทำได้ทั้งคู่ แต่ CLI ยังติดตั้ง package กับ peer dependency ให้ถูกต้องด้วย — config ที่แก้เองแล้วขาด dependency จะพังตรงจุดที่งงกว่า (resolution error ลึกใน build) ไม่ใช่ที่ config เอง
🐛 หา bug ในโค้ดนี้
// astro.config.mjs — both integrations installed, no include/excludeimport { defineConfig } from 'astro/config';import preact from '@astrojs/preact';import react from '@astrojs/react';
export default defineConfig({integrations: [preact(), react()],});
// src/components/RefDemo.jsx — meant to be the React-only exampleimport { useRef } from 'react';export default function RefDemo() { /* ... */ }เฉลย
ทั้งสอง integration อ้างสิทธิ์ .jsx เหมือนกันตรงนี้ — ไม่มีตัวไหนมี include/exclude แยกไว้เลย พิสูจน์ข้างบน: config แบบนี้ warn ทุก build ([@astrojs/preact] More than one JSX renderer is enabled) และแย่กว่านั้นคือ renderer ตัวไหนจะได้ RefDemo.jsx ไปจริง ๆ ไม่ได้การันตีว่าจะเป็น React แค่เพราะโค้ด import จาก 'react' — Astro เลือกตาม ลำดับการลงทะเบียน/การ match ไม่ได้ดูจาก import ของไฟล์เอง ทางแก้คือการแยกแบบไม่ทับกันที่พิสูจน์ไว้ข้างบน: preact({ include: ['**/*.tsx'] }) กับ react({ include: ['**/*.jsx'] }) ทำให้ไฟล์ .jsx เป็นของ React แน่นอน และไฟล์ .tsx เป็นของ Preact แน่นอน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”integration ไม่ได้ “เป็นเจ้าของ” framework แบบ global — มันเป็นเจ้าของ ชุด pattern ของไฟล์ (ผ่าน include/exclude ซึ่ง default คือทุกไฟล์ที่ Vite plugin ของ framework นั้นจัดการได้) integration สองตัวที่ pattern ทับกันคือสองคำกล่าวอ้างบนไฟล์ชุดเดียวกัน แล้ว Astro ต้องเลือกสักตัว — นั่นคือความกำกวมที่พิสูจน์ไว้ข้างบนพอดี การระบุ pattern ชัดเจนคือสิ่งที่ทำให้ “renderer ไหนจัดการไฟล์นี้” เป็นข้อเท็จจริงจาก config ของคุณ ไม่ใช่การเดาที่ Astro ทำแทนคุณ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”framework เดียว ไม่มี include/exclude |
หลาย framework แยก include/exclude ไม่ทับกัน |
|
|---|---|---|
| ความซับซ้อนของ config | ง่ายที่สุด — แค่ integrations: [preact()] |
หนึ่ง pattern include ต่อ integration |
| นามสกุลไฟล์กำกวมไหม | ไม่มี — มี JSX-capable integration แค่ตัวเดียว | ไม่มี ตราบใดที่ pattern ไม่ทับกัน |
| ถ้าข้ามขั้นตอนนี้จะพังอะไร | ไม่พังอะไร (ไม่มีอะไรให้ชนกัน) | build warning จริง และ renderer ที่ถูกเลือกต่อไฟล์ที่คาดเดาไม่ได้ |
| เหมาะกับ | ทุกบทในคอร์สนี้ และโปรเจกต์จริงส่วนใหญ่ | ตั้งใจผสม ecosystem (migrate, component ที่มีเฉพาะ framework เดียว) |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Astro เลือก renderer ที่ถูกต้องให้ไฟล์ .jsx ตามสิ่งที่มัน import” พิสูจน์ข้างบน: ตอน integration ทับกันสองตัวและไม่มี
include/excludeการเลือกไม่ได้อิงจากimport { ... } from 'react'ของไฟล์เอง — อิงจากลำดับการลงทะเบียน/match ของ integration และมันจะ warn แทนที่จะเดาให้ถูกแบบเงียบ ๆ - “React 19 feature ต้องมี Astro รองรับพิเศษถึงจะใช้ใน island ได้” พิสูจน์ข้างบน:
refเป็น prop กับuseActionStateทำงานได้เองเลย เพราะ island ของ React คือ React 19 tree ธรรมดาข้างใต้ Astro ไม่ต้องทำอะไรพิเศษให้ feature ของภาษา/library ที่อยู่ข้างใน render ของ island ตัวเองทั้งหมด - “function action ของ
useActionStateรันบน server เหมือน Server Action ของ Next.js” พิสูจน์ข้างบน: มันเป็น async function ฝั่ง client ธรรมดา — ไม่มีสะพาน RSC ไม่มี network request ไม่มีอะไรที่เป็น server-side เลยข้างใน client island ธรรมดา - “กฎ serializable-props เป็นข้อจำกัดเฉพาะของ React” มันใช้ที่ boundary ของ island เอง สำหรับทุก framework ที่ Astro รองรับ — พิสูจน์เหมือนกันทั้งกับ Preact ในบท client-directives และกับ React ในบทนี้