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

Project & Tooling

หลายปีที่ผ่านมา คำตอบมาตรฐานของ “เริ่ม React app ยังไง?” คือ Create React App (CRA) ยุคนั้นจบแล้ว react.dev พูดตรง ๆ เลยว่า:

No. Create React App has been deprecated. For more information, see Sunsetting Create React App. — react.dev/learn/installation

และ blog post เองก็บอกชัดว่าต้องทำอะไรแทน:

Today, we’re deprecating Create React App for new apps, and encouraging existing apps to migrate to a framework, or to migrate to a build tool like Vite, Parcel, or RSBuild. — react.dev/blog

ตอนนี้ติดตั้ง CRA จะขึ้น warning ชี้ไปยังตัวเลือกปัจจุบัน อย่าเริ่มโปรเจกต์ใหม่ด้วย CRA และควรพิจารณาย้ายโปรเจกต์เดิมออก

ตัวแทนคือทางแยก: build tool สำหรับ client-side SPA หรือ framework สำหรับอะไรก็ตามที่ต้องการ routing, server rendering หรือ Server Components

สำหรับ SPA แบบ client-rendered Vite คือมาตรฐาน เช็ค currency ก่อนเสมอ — รันเองก่อน copy ตัวเลขจาก tutorial ไหนก็ตาม เพราะ command นี้แหละที่ทำให้บทนี้มีวันที่กำกับ:

Terminal window
npm view vite version # 8.3.1 at the time of writing
npm view @vitejs/plugin-react version # 6.1.1
Terminal window
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

output จริงจาก command นั้นเป๊ะ ๆ ตอนนี้:

> npx
> 'create-vite' my-app --template react-ts
◇ Scaffolding project in .../my-app...
└ Done. Now run:
cd my-app
npm install
npm run dev

และ package.json ที่มันเขียนออกมา:

{
"scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", "preview": "vite preview" },
"dependencies": { "react": "^19.2.8", "react-dom": "^19.2.8" },
"devDependencies": {
"@vitejs/plugin-react": "^6.1.1",
"oxlint": "^1.81.0",
"typescript": "~6.0.2",
"vite": "^8.3.0"
}
}

จุด currency สองจุดที่ทำให้คนที่ copy tutorial เก่ามาสะดุด:

  • template ไม่มี ESLint ให้แล้ว มันให้ oxlint แทน — linter ที่เขียนด้วย Rust, config ผ่าน .oxlintrc.json, เปิด react/rules-of-hooks มาให้เลย ถ้าอยากได้ compiler-aware rule ของ eslint-plugin-react-hooks (ด้านล่าง) ต้องเพิ่ม ESLint เอง ไม่ได้มาให้ default อีกต่อไป

  • @vitejs/plugin-react v6 เปลี่ยน transform doc install ของ react.dev เองบอกชัด:

    @vitejs/plugin-react uses Oxc. In @vitejs/[email protected], the inline Babel option was removed. — Vite’s create-vite React template README

    ก่อน v6 plugin ใช้ Babel สำหรับ Fast Refresh และ JSX ตั้งแต่ v6 เป็นต้นไป มันใช้ Oxc เป็น transform default — ติดตั้งเล็กลง ไม่ต้องพึ่ง Babel สำหรับกรณีทั่วไป @vitejs/plugin-react-swc ยังเป็นอีกตัวเลือกแยกต่างหาก: สลับไปใช้ SWC โดยเฉพาะเพื่อเร่ง cold start และ HMR บนโปรเจกต์ใหญ่มาก เลือก Oxc (default) ไว้ก่อน เว้นแต่มีเหตุผลเฉพาะที่ต้องใช้ SWC — react() เปล่า ๆ ไม่มี Babel plugin ตอนนี้คือทางที่เร็วแล้ว ไม่ใช่ทางช้าเหมือนก่อน

    guide build-a-react-app-from-scratch ของ react.dev เองก็พูดถึง RSbuild (ขับเคลื่อนด้วย Rspack) เป็นทางเลือกอีกอันคู่กับ Vite — ควรรู้จักชื่อไว้ แม้คอร์สนี้จะยึด Vite เป็นมาตรฐาน

บทเรียน the-react-compiler ในโมดูล React 19 โชว์ Babel transform output จริงไปแล้ว — อ่านอันนั้นก่อนถ้ายังไม่ได้อ่าน ส่วนนี้พิสูจน์ การเดินสาย install ให้ทำงานจริง ซึ่งบทนั้นไม่ได้ครอบคลุม

เพราะ @vitejs/[email protected]+ เอา option babel: {...} แบบ inline ออกไปแล้ว การเดินสาย compiler เลยต้องมี plugin ตัวที่สอง @rolldown/plugin-babel บวก helper reactCompilerPreset ที่ @vitejs/plugin-react v6 export มาให้เพื่อการนี้โดยเฉพาะ:

Terminal window
npm install -D babel-plugin-react-compiler @rolldown/plugin-babel
// vite.config.ts — illustrative; not type-checked here (needs @rolldown/plugin-babel + babel-plugin-react-compiler, which the shared course probe doesn't install — the real, separate build proof is in the terminal output right below)
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'
export default defineConfig({
plugins: [
react(),
babel({ presets: [reactCompilerPreset()] }),
],
})

นี่ไม่ใช่ config ที่แค่ parse ผ่าน — มันถูกรันเต็มรูปแบบใน probe ของคอร์สนี้กับ Vite scaffold ค่า default npm run build (tsc -b && vite build):

vite v8.3.1 building client environment for production...
✓ 22 modules transformed.
dist/assets/index-C9S9Cn0y.js 223.97 kB │ gzip: 69.84 kB
✓ built in 252ms

exit 0 และเมื่อตั้ง build.minify: false เพื่ออ่าน bundle ได้ ตัว output มี compiler runtime import อยู่จริง:

$ grep -c "compiler-runtime" dist/assets/index-*.js
2

สองจุดอ้างอิง — หนึ่งคือ import เอง อีกหนึ่งคือ entry ที่ reactCompilerPreset เติมให้ optimizeDeps.include อัตโนมัติ (react/compiler-runtime) นั่นคือ install ทั้งหมด: dev dependency สองตัว, plugin entry เพิ่มมาหนึ่งอัน แล้ว compiler ก็รันบน build จริงของคุณ — ไม่ต้องมี step แยก ไม่มี surprise ใน watch mode

เพราะ Vite template ไม่มี ESLint มาให้แล้ว เพิ่มเอง — แล้วเพิ่ม plugin ที่ให้ทั้ง Hooks rule คลาสสิกและ diagnostic ของ compiler เองมาเป็น lint error:

Terminal window
npm install -D eslint @typescript-eslint/parser eslint-plugin-react-hooks
eslint.config.mjs
import reactHooks from 'eslint-plugin-react-hooks';
import tsParser from '@typescript-eslint/parser';
import { defineConfig } from 'eslint/config';
export default defineConfig([
{ files: ['**/*.{ts,tsx}'], languageOptions: { parser: tsParser } },
reactHooks.configs.flat['recommended-latest'],
]);

recommended-latest ([email protected] เวอร์ชันที่ probe ของคอร์สนี้ติดตั้งไว้) เปิดทั้ง rule-of-hooks และ rule เฉพาะของ compiler ในชุด preset เดียว — ไม่ต้องเพิ่ม package แยกสำหรับ compiler เลย นี่คือการ violate จริงสองแบบ รันผ่าน config ด้านบนเป๊ะ ๆ:

Bad.tsx
import { useState } from 'react';
export function Toggle({ show }: { show: boolean }) {
if (show) {
const [on, setOn] = useState(false); // hook called conditionally
return <button onClick={() => setOn(!on)}>{String(on)}</button>;
}
return null;
}
Bad.tsx
5:25 error React Hook "useState" is called conditionally. React Hooks must be called
in the exact same order in every component render. Did you accidentally call a React
Hook after an early return? react-hooks/rules-of-hooks
Counter.tsx
import { useEffect, useState } from 'react';
export function Counter({ step }: { step: number }) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((c) => c + step), 1000);
return () => clearInterval(id);
}, []); // missing dependency: step
return <output>{count}</output>;
}
Counter.tsx
8:6 warning React Hook useEffect has a missing dependency: 'step'. Either include
it or remove the dependency array. If 'setCount' needs the current value of 'step',
you can also switch to useReducer instead of useState and read 'step' in the
reducer react-hooks/exhaustive-deps

ทั้งสองอันจริง ทั้งสองอันมาจาก config ข้างบนตรง ๆ — ไม่ได้เขียน error text เอง

probe ของคอร์สนี้เอง (และทุก scaffold ข้างบน) เปิด strict บวกอีกสอง setting ที่ควรรู้จักชื่อไว้ เพราะมันเปลี่ยนความหมายของ “TypeScript ที่ valid” ใน setup นี้โดยเฉพาะ:

  • erasableSyntaxOnly — ปฏิเสธ construct ที่มีแค่ใน TS runtime (enum, constructor parameter property) ที่ erase ไปเป็นค่าว่างในระดับ type ไม่ได้ Node สมัยใหม่รัน .ts ตรง ๆ ได้ด้วยการ strip type ออก flag นี้ทำให้ code ของคุณเข้ากันได้กับแบบนั้นโดยโครงสร้าง
  • noUnusedLocals / noUnusedParameters — import หรือ prop ที่ destructure มาแล้วไม่ใช้คือ compile error ไม่ใช่แค่ lint warning code ที่ generate มาแล้วมี import เหลือค้างจะ build พังตรงนี้ ไม่ใช่แค่ eslint บ่น

บทเรียน TypeScript with React ลงลึกเรื่อง type component, hooks และ event ภายใต้ strict config ชุดเดียวกันนี้

เมื่อไรที่ต้องการ routing จริง, server-side rendering, data loading หรือ React Server Components ให้หยิบ framework มาใช้แทนการประกอบเอง คำแนะนำปัจจุบันจาก React docs:

Terminal window
npx create-next-app@latest # Next.js — App Router, RSC, SSR
npx create-react-router@latest # React Router (framework mode) — routing + SSR
flowchart TB
  q{"ต้องการ routing / SSR /
Server Components?"}
  q -->|no, client SPA| vite["Vite
(build tool, Oxc transform)"]
  q -->|yes| fw["Framework:
Next.js or React Router"]
  cra["Create React App"] -->|deprecated 2025| x["do not use"]
เลือกวิธีเริ่ม

framework ดูแลเรื่องที่ build tool ทิ้งให้คุณจัดการ: nested route, data fetching บน server, streaming และ RSC boundary (อยู่ในโมดูล React 19) สำหรับ SPA แบบ dashboard หลัง login Vite เพียงพอ; สำหรับ content site หรืออะไรที่เน้น SEO หรือ server-data ก็คุ้มที่จะใช้ framework

ไม่ว่าเลือกอันไหน สิ่งเหล่านี้ทำให้การพัฒนา React สนุก:

  • React DevTools (browser extension) — ดู component tree, props, state และ hooks; Profiler บอกว่าทำไมและ component render บ่อยแค่ไหน (สำคัญมากในโมดูล performance)
  • Fast Refresh — แก้ component แล้วเห็นผลทันที โดย เก็บ state ของ component ไว้ built-in ใน Vite และ framework
  • JSX transform สมัยใหม่ — ไม่ต้อง import React from 'react' ที่หัวทุกไฟล์แค่เพื่อใช้ JSX อีกต่อไป; compiler inject runtime import ให้ (ยังต้อง import hooks และ API ที่ใช้)
  • Strict Mode — ครอบ app ด้วย <StrictMode> ใน development เพื่อจับ render ที่ไม่ pure และบั๊ก effect (การ double-invoke ที่คุณเจอใน Foundations)

tooling พวกนี้ไม่ใช่ React เลย React ship แค่ rendering library เท่านั้นโดยตั้งใจ — ทุกตัวเลือกในบทนี้ (Oxc เทียบ SWC transform, ESLint เทียบ oxlint, Vite เทียบ framework) คือคำตอบที่ community เห็นตรงกันว่า “จะใส่อะไรรอบ ๆ React” ไม่ใช่ API ของ React เอง อ่าน currency claim ในบทนี้แบบเดียวกัน: เป็น “สิ่งที่ลงตัวกัน ณ เลขเวอร์ชันที่โชว์ไว้” เช็คซ้ำได้ด้วย command npm view / scaffold เป๊ะ ๆ ที่ให้ไว้ ไม่ใช่สิ่งที่ต้องจำ

ตัวเลือกได้อะไรต้นทุน
Vite + @vitejs/plugin-react (Oxc, default)ติดตั้งเล็กลง ไม่ต้องพึ่ง Babel ทางที่เร็วที่สุดสำหรับกรณีทั่วไปไม่มี Babel plugin แบบ inline — compiler ต้องเดินสายแยกผ่าน @rolldown/plugin-babel ตามที่โชว์ข้างบน
@vitejs/plugin-react-swccold start/HMR เร็วกว่าบน codebase ใหญ่มากต้องรู้จัก transform ตัวที่สองเพิ่ม ไม่จำเป็นถ้า Oxc ยังไม่ใช่คอขวดจริง
framework (Next.js / React Router) แทน Viterouting, SSR, RSC จัดการให้ต้องเรียนรู้เพิ่ม โครงสร้างมี opinion มากกว่า บ่อยครั้งเกินความจำเป็นสำหรับ SPA เล็ก ๆ
oxlint (default ของ scaffold) เทียบเพิ่ม ESLint + eslint-plugin-react-hooksoxlint เร็วทันที ไม่ต้อง config มี react/rules-of-hooks มาให้เฉพาะ ESLint recommended-latest เท่านั้นที่ให้ diagnostic ของ React Compiler เองเป็น lint error
  • “CRA เก่าแต่ยังใช้ได้กับโปรเจกต์เล็ก ๆ” guide install ของ react.dev เองตอบตรง ๆ: “No. Create React App has been deprecated.” ไม่ใช่แค่เรื่องรสนิยมอีกต่อไป — doc พาคุณออกห่างจากมันจริง ๆ
  • “เพิ่ม @vitejs/plugin-react แล้วได้ Babel มาด้วย” ไม่ใช่ตั้งแต่ v6 — ทางเดิน default คือ Oxc ไม่มี Babel dependency เลย คุณเพิ่ม Babel กลับมาเอง (ผ่าน @rolldown/plugin-babel) ก็ต่อเมื่อต้องการ plugin ที่เป็น Babel-only อย่าง React Compiler
  • “Vite React template มี ESLint มาให้” มันมี oxlint ให้ ถ้าโปรเจกต์ที่ generate มามี eslint.config.js ที่คุณไม่ได้เพิ่มเอง เช็คว่ามันรันจริงไหม — config ที่ว่างเปล่าหรือค้างเก่าแย่กว่าไม่มี config เพราะดูเหมือนกำลัง lint อยู่ทั้งที่ไม่ได้ทำอะไร
  • โปรเจกต์ยังมีไฟล์ .eslintrc.* แบบเก่าอยู่คู่กับ ESLint เวอร์ชันปัจจุบันไหม? ESLint 9+ เป็น flat-config-only โดย default และปฏิเสธที่จะรันเลยถ้าเจอ .eslintrc.* โดยไม่มี eslint.config.* — นี่ไม่ใช่เรื่อง style แต่คือ fail แบบเต็ม ๆ
  • ถ้า vite.config.ts ส่ง react({ babel: { plugins: [...] } }) เช็คเวอร์ชัน @vitejs/plugin-react ที่ติดตั้งอยู่ — option นั้นไม่มีตั้งแต่ v6.0.0 เป็นต้นไป และขึ้นอยู่กับ type ที่ใช้ การส่ง option นี้อาจเป็น silent no-op แทนที่จะ error ตอน build
  • hook ถูกเรียกหลัง return แบบ early หรือในนั้น if ไหม? react-hooks/rules-of-hooks จะจับได้ก็ต่อเมื่อ config ถูกตั้งและรันจริง — เช็คว่า lint command ถูกเดินสายเข้า CI แล้ว ไม่ใช่แค่มีอยู่ใน package.json
  • eslint-plugin-react-hooks ตั้งเป็น recommended (แค่ rules-of-hooks + exhaustive-deps) ทั้งที่โปรเจกต์อยากได้ diagnostic ของ React Compiler ด้วย (immutability, purity, set-state-in-render, …) ไหม? อันหลังมาจาก recommended-latest เท่านั้น

🐛 หา bug ในโค้ดนี้

eslint.config.js
import react from '@vitejs/plugin-react'
export default {
plugins: [
react({ babel: { plugins: ['babel-plugin-react-compiler'] } }),
],
}
เฉลย

นี่คือวิธีเดินสาย React Compiler เข้า @vitejs/plugin-react แบบก่อน v6.0.0 ที่ถูก copy มาใช้ในโปรเจกต์ที่รัน plugin เวอร์ชันปัจจุบัน เช็คตรง ๆ กับ type definition ของ @vitejs/[email protected] เอง: Options ไม่มี field babel อีกต่อไป — การส่ง option นี้เข้าไปจะ type-check ไม่ผ่านภายใต้ strict TypeScript หรือไม่ก็ถูกเมินเฉยตอน runtime แล้วแต่ config เขียนแบบไหน ไม่ว่าทางไหน compiler ก็ไม่เคยรัน โดยไม่มี error บอกเลย การเดินสายปัจจุบันต้องมี plugin ตัวที่สองแยกต่างหาก: @rolldown/plugin-babel โดยส่ง compiler เข้าไปเป็น preset ผ่าน reactCompilerPreset() — พิสูจน์ด้วย vite build จริงก่อนหน้านี้ในบทเรียน (grep -c "compiler-runtime" dist/assets/index-*.js → 2 ยืนยันว่า transform รันจริง) เช็ค npm ls @vitejs/plugin-react ก่อน copy install snippet เสมอ — นี่คือรายละเอียดที่ล้าสมัยเร็วมากระหว่าง major version ของ plugin

สถานะปัจจุบันของ Create React App ตามที่ react.dev บอกเองคืออะไร?
ตั้งแต่ @vitejs/plugin-react v6 plugin ใช้อะไรเป็น transform default?
ทำไม config นี้ถึงไม่เปิด React Compiler บนเวอร์ชันนี้?
// vite.config.ts, @vitejs/[email protected]
import react from '@vitejs/plugin-react'
export default { plugins: [react({ babel: { plugins: ['babel-plugin-react-compiler'] } })] }
Vite template react-ts มี linter อะไรมาให้ default ตอนนี้?
recommended-latest ของ eslint-plugin-react-hooks รายงานอะไรสำหรับโค้ดนี้ ยืนยันด้วยการรันจริง?
export function Toggle({ show }: { show: boolean }) {
  if (show) {
    const [on, setOn] = useState(false);
    return <button onClick={() => setOn(!on)}>{String(on)}</button>;
  }
  return null;
}
react.dev quote อะไรเป็นทางเลือกแทน Create React App นอกจาก Vite?