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

Framework Integrations

Astro ไม่มี client-side component runtime ของตัวเอง — แต่ให้คุณใช้ UI framework ที่คุณรู้จักอยู่แล้วกับส่วนที่ต้อง interactive แทน ทั้ง React, Vue, Svelte, Solid และ Preact ใช้เป็น island ได้หมด ผ่าน integration อย่างเป็นทางการ

เพิ่ม integration ด้วย CLI ซึ่งจะติดตั้ง package และตั้งค่า config ให้:

Terminal window
npx astro add react

คำสั่งนั้นอัปเดต astro.config.mjs ให้เอง:

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 ได้แล้ว

island เขียนด้วยสไตล์ปกติของ framework นั้น ส่วนไฟล์ .astro เป็นตัว import และตัดสินใจว่า hydrate เมื่อไหร่

// src/components/Counter.jsx — a plain React component
import { useState } from 'react';
export default function Counter({ start }) {
const [count, setCount] = useState(start);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
src/pages/index.astro
---
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 ของตัวเอง

เพราะแต่ละ 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"]
หน้าเดียว มี island จาก framework ต่างตัวกัน

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 unexpected
behavior unless you set the include or exclude option. See
https://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 จากไฟล์ .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, Date object ในบางกรณี และค่าที่ไม่ใช่ 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

@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 prop
export default function ReactField({ ref }) {
return <input ref={ref} data-testid="react-field" defaultValue="" />;
}
RefDemo.jsx
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:

ActionForm.jsx
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

ตอนเจอ 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/exclude
import { 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 example
import { 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 แน่นอน

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 ในบทนี้
จะเพิ่มการรองรับ component React ในโปรเจกต์ Astro อย่างไร?
ใช้ React island กับ Svelte island ในหน้าเดียวกันได้ไหม?
ทำไมส่ง function (เช่น event handler) เป็น prop ให้ island จากไฟล์ .astro ไม่ได้?
framework component ที่ import เข้ามาในไฟล์ .astro โดยไม่มี client directive จะ:
warning จริงตัวนี้ปรากฏเมื่อโปรเจกต์มี:
[@astrojs/preact] More than one JSX renderer is enabled. This will lead to unexpected
behavior unless you set the include or exclude option.
ใช้ข้างใน island ของ Astro ที่ hydrate ด้วย client:load ทำงานได้ไหม?
// src/components/ReactField.jsx — React 19, no forwardRef
export default function ReactField({ ref }) {
  return <input ref={ref} />;
}