Skip to content

บทที่ 1 — First React App: Component, JSX, Props, State, Event

← บทที่ 0 | สารบัญ

🟢 ระดับ: มือใหม่

บทนี้เราจะ:

  1. สร้าง project React ด้วย Vite
  2. ดู structure ของ project + เข้าใจไฟล์แต่ละตัว
  3. เขียน component แรก
  4. เข้าใจ JSX ทุกบรรทัด — กฎที่ต่างจาก HTML
  5. ใช้ props, state, event
  6. ทำ TODO list เล็ก ๆ
  7. บทนี้สร้าง project ด้วย TypeScript ตั้งแต่แรก (ใช้ template Vite TS — ทบทวน TypeScript ได้ที่บท 0 §4 ถ้าต้องการ หรือใช้ template --template react (JS เปล่า) แทนก็ได้)

1. ติดตั้ง Node.js

React ต้องการ Node.js (runtime — ตัวรัน JavaScript นอก browser) และจะใช้ npm (Node Package Manager — เครื่องมือติดตั้ง/จัดการ library)

📖 LTS (Long-Term Support = รุ่นที่ได้รับการดูแลระยะยาว — รุ่นที่แนะนำสำหรับงาน production)

ตรวจก่อน:

bash
node --version
npm --version

ถ้าไม่มี:

  • Windows: winget install OpenJS.NodeJS.LTS (winget มีมาให้ใน Windows 11 หรือ Windows 10 ที่อัปเดตแล้ว — ถ้าไม่มี winget ให้ดาวน์โหลด Node.js ตรงจาก https://nodejs.org แทน)
  • macOS: brew install node
  • Linux: ผ่าน nvm

💡 winget, brew, nvm = package manager (ตัวจัดการแพ็กเกจ — เครื่องมือลงโปรแกรมในเครื่องผ่าน command line ทำหน้าที่คล้าย Microsoft Store / App Store)

ต้องการ Node 22+ (LTS ปัจจุบัน — แนะนำ Node 22 LTS หรือ Node 24)

💡 ถ้าทำงานหลาย project ที่ต้องการ Node version ต่างกัน แนะนำใช้ nvm (Linux/macOS) หรือ fnm (cross-platform — ทุก OS) เพื่อจัดการหลาย Node version แทนการ install ตรง ๆ — ค้นหา "fnm fast node manager" หรือ "nvm-windows" ได้เลย


2. สร้าง project ด้วย Vite

💡 Vite (อ่าน "วีท") = เครื่องมือ build + dev server สำหรับ React/Vue/Svelte สมัยใหม่ เร็วกว่า Webpack/CRA รุ่นเก่ามาก (ใช้ esbuild สำหรับ dependency pre-bundling และ Rollup (หรือ Rolldown ถ้าเปิด opt-in) สำหรับ production build — เป็นเครื่องมือ compile/bundle เขียนด้วย Go/Rust (ภาษาโปรแกรมอื่น ๆ ที่เร็วมาก — ไม่ต้องรู้ก็ใช้ Vite ได้) — ภายใน) — เป็นมาตรฐานของ React project ใหม่ปี 2026

📟 เปิด terminal ก่อน — Windows: เปิด PowerShell หรือ Windows Terminal (ค้นจาก Start menu); macOS: เปิด Terminal.app; Linux: terminal ของระบบ ใน terminal คุณพิมพ์คำสั่งทีละบรรทัดแล้วกด Enter เพื่อรัน

คำสั่ง cd = "change directory" = เปลี่ยน folder ปัจจุบันที่ terminal ทำงานอยู่ (เช่น cd C:\code = ไปอยู่ที่ folder C:\code)

bash
# cd ไป folder ที่อยาก create project (เช่น ~/code/ หรือ C:\code\)
npm create vite@latest myapp -- --template react-ts

อธิบายคำสั่ง:

  • npm create vite@latest = เรียก script create-vite รุ่นล่าสุด
  • myapp = ชื่อ folder ที่จะสร้าง
  • -- (ขีดสองตัว) = ตัวคั่นพิเศษ — ใส่ตามนี้ได้เลย ไม่ต้องสนใจรายละเอียด
  • --template react-ts = ใช้ template "React + TypeScript"

ถ้ายังไม่ถนัด TS → ใช้ --template react (JS เปล่า) ก่อนก็ได้ — เนื้อหาในบทยังตามได้ ข้าม TS syntax (: type, <T>) ไป

bash
cd myapp
npm install
npm run dev

ต้องเห็น:

text
  VITE v7.x.x  ready in 234 ms

  ➜  Local:   http://localhost:5173/

⚠️ ปี 2026 Vite ใช้ v7+ — output อาจต่างเล็กน้อยจากที่แสดง (ไม่กระทบเนื้อหา) รายละเอียดเรื่อง bundler (เครื่องมือรวมไฟล์ JS หลายไฟล์เป็นไฟล์เดียวให้ browser โหลด — Rollup คือรุ่นเดิม, Rolldown คือรุ่นใหม่ที่ Vite กำลังเปิด opt-in) ข้ามได้สำหรับมือใหม่

เปิด browser → http://localhost:5173

⚠️ อย่าปิด terminal — dev server ต้องรันอยู่ตลอดที่คุณกำลังพัฒนา ถ้าปิด terminal แล้วอยากรัน server ใหม่ให้รัน npm run dev อีกครั้ง

🎉 เห็น React + Vite logo + counter — app run แล้ว


3. ดู project structure

text
myapp/
├── package.json              ← dependency + script
├── vite.config.ts            ← config Vite
├── tsconfig.json             ← config TypeScript
├── index.html                ← root HTML (มี <div id="root">)
├── public/                   ← static files
├── src/
│   ├── main.tsx              ← entry — render App ลงใน <div id="root">
│   ├── App.tsx               ← root component
│   ├── App.css               ← style ของ App
│   ├── index.css             ← style ทั่วไป
│   ├── assets/
│   └── vite-env.d.ts         ← Vite type definitions
└── node_modules/             ← dependencies — folder ที่ npm ดาวน์โหลด library ทุกตัวลงมาเก็บ (อาจมีหลายพันไฟล์ — ปกติ อย่าแก้ — และห้าม commit เข้า git (ไฟล์ .gitignore ที่ Vite สร้างให้จะ ignore folder นี้ให้อัตโนมัติ))

3.1 ดู index.html

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

แค่ <div id="root"></div> — React จะใส่ทุกอย่างที่นี่

3.2 ดู main.tsx

tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

วิเคราะห์:

  • createRoot(...) — สร้าง "ราก" ของ React app ตรง DOM element ที่ระบุ (ที่นี่คือ <div id="root">)
  • getElementById('root')!! ตอนท้ายเป็น TypeScript non-null assertion (การยืนยันว่าค่านี้ไม่เป็น null — การันตีให้ TS แต่ถ้าจริง ๆ เป็น null จะเกิด runtime error) = บอก TS ว่า "รับรองว่าไม่ null" ในกรณีนี้ปลอดภัย เพราะไฟล์ index.html มี <div id="root"> อยู่แน่ ๆ แล้ว (ถ้าใช้ template JS เปล่า ๆ ไม่มี ! ตัวนี้) — เจอ syntax ใหม่ตรงนี้ที่ยังไม่เห็นในบท 0 (§4 ยังไม่ครอบคลุม !) อธิบายทันทีตรงนี้เลย
  • .render(<StrictMode><App /></StrictMode>) — สั่ง React วาด component นี้
  • <App /> — root component (เริ่มต้นของ tree)
  • <StrictMode> — เปิด check เพิ่ม (เฉพาะ dev — ไม่เห็นใน production)

🤔 <StrictMode> ทำให้ render สองครั้ง — ปกติไหม?

ปกติ! ใน dev mode (รัน npm run dev) StrictMode ตั้งใจเรียก component ซ้ำเพื่อช่วยจับ bug ที่อาจซ่อนอยู่ — เห็น console.log('render') ออก 2 ครั้ง = ปกติ ใน production build (npm run build) จะออกครั้งเดียว

รายละเอียดเชิงลึก (ข้ามได้ตอนนี้): StrictMode ช่วย detect side effect ใน render, missing cleanup ใน useEffect, deprecated API — รายละเอียดในบท 2 (Hooks) และบท 14 (Internals) เก็บ StrictMode ไว้เสมอ — มันช่วยจับ bug ก่อน user เจอ

3.3 ดู App.tsx

tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'  // path ขึ้นต้นด้วย / = ไฟล์ใน public/ folder (root ของ project)
import './App.css'                // ./ = path สัมพัทธ์จากไฟล์ปัจจุบัน

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        {/* ... = โค้ดที่เหลือของ template (React logo + link) — ยังไม่สำคัญตอนนี้ */}
      {/* ⚠️ โค้ดบล็อกนี้เป็นตัวอย่างสำหรับอ่านทำความเข้าใจเท่านั้น — อย่าคัดลอกไปแทนไฟล์จริง เพราะตัดโค้ดบางส่วนออก */}
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
      </div>
    </>
  )
}

export default App

อ่าน:

  • function App() { ... return JSX } — component คือ function
  • useState(0) — สร้าง state
  • return (...) — return JSX
  • export default App — export

4. JSX — รายละเอียดที่ทุกคนต้องรู้

JSX (อ่าน "เจ-เอส-เอ็กซ์") = syntax extension ของ JavaScript — เขียน HTML-like ใน JS ได้

jsx
const element = <h1>Hello, World!</h1>;

Vite compile (แปลงโค้ดให้กลายเป็น JS ที่ browser อ่านได้ — เกิดอัตโนมัติเมื่อ save ไฟล์) โดยใช้ Babel (เครื่องมือแปลง JSX → JS ที่ browser รู้จัก — ทำงานอัตโนมัติผ่าน Vite) — React 17+ ใช้ new JSX transform: compile เป็น _jsx() จาก react/jsx-runtime (ไม่ต้อง import React from 'react' อีกต่อไป — Vite template ใหม่จะ config ให้แล้ว) ซึ่งเทียบเท่าผล:

js
const element = React.createElement('h1', null, 'Hello, World!');

JSX ดูเหมือน HTML แต่จริงเป็น JS — มี กฎ ที่ต่าง

4.1 ใส่ JavaScript ใน { }

jsx
const name = "Anna";
const age = 25;

return (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age * 2}</p>
    <p>Status: {age >= 18 ? "Adult" : "Minor"}</p>
  </div>
);

ทุกอย่างใน {} คือ JS — สามารถใส่ expression (นิพจน์ — โค้ดที่มีค่าคืนออกมา เช่น 1+1, name.toUpperCase() (แปลง name เป็นตัวพิมพ์ใหญ่ทั้งหมด), age >= 18 ? "Adult" : "Minor") ใด ๆ ก็ได้ แต่ใส่ statement (คำสั่ง — เช่น if, for ที่ไม่คืนค่า) ไม่ได้

4.2 ใช้ className ไม่ใช่ class

jsx
<div className="card">    {/* ✅ */}
<div class="card">        {/* ❌ class เป็น keyword JS */}

4.3 ใช้ camelCase สำหรับ attribute

jsx
<input type="text" onChange={handler} maxLength={10} />
{/* ไม่ใช่ onchange, maxlength */}

4.4 self-close tag

jsx
<img src="..." />          {/* ✅ */}
<input type="text" />      {/* ✅ */}
<br />                     {/* ✅ */}
<img src="...">            {/* ❌ ต้อง self-close */}

4.5 ต้อง wrap ในตัวเดียว

jsx
// ❌ error — compile ไม่ผ่าน ("Adjacent JSX elements must be wrapped...")
return (
  <p>First</p>
  <p>Second</p>
);

// ✅ wrap ใน div
return (
  <div>
    <p>First</p>
    <p>Second</p>
  </div>
);

// ✅ ใช้ fragment (ไม่เพิ่ม element)
//    Fragment = tag ว่างพิเศษ <>...</> ใช้ wrap หลาย element โดยไม่เพิ่ม <div> จริงใน HTML output
return (
  <>
    <p>First</p>
    <p>Second</p>
  </>
);

4.6 inline style เป็น object

jsx
<div style={{ color: 'red', fontSize: '20px' }}>Hello</div>
{/* ใส่ double {{ }} เพราะ outer = JS expression, inner = object */}

camelCase + ค่าเป็น string

4.7 conditional rendering

jsx
{isLoggedIn ? <Dashboard /> : <Login />}    // ternary

{isLoggedIn && <Dashboard />}                // ถ้า true → render, ถ้า false → ไม่

{count > 0 ? <p>{count} items</p> : null}    // ไม่ render = null

⚠️ 0 && <X/> จะ render 0 เพราะ 0 เป็น falsy (= ค่าที่ถือว่าเป็น false ใน JS เช่น 0, "", null, undefined, false) แต่ 0 render ได้ — ใช้ count > 0 ? แทน

4.8 render list

jsx
const fruits = ["apple", "banana", "cherry"];

return (
  <ul>
    {fruits.map(f => <li key={f}>{f}</li>)}
  </ul>
);

key prop สำคัญมาก — React ใช้ key บอกว่า "item ตัวไหนคือตัวไหน" เวลา list เปลี่ยน (เพิ่ม/ลบ/สลับลำดับ) ถ้าไม่มี key React อาจ render ผิด (เช่น เก็บ state ของ item ผิดตัว) หรือช้า (re-render ทั้งหมดแทนเฉพาะที่เปลี่ยน)

  • ใช้ id เสมอ ถ้ามี
  • index ใช้ได้ถ้า list เป็น static (ไม่มีการเพิ่ม/ลบ/สลับลำดับ) เช่น tab menu คงที่ — ถ้า list มี form input ห้ามใช้ index เด็ดขาด เพราะอาจ map state ผิดตัวหลัง delete

5. Component — function ที่ return JSX

📖 คำศัพท์ที่จะเจอตั้งแต่ §5 เป็นต้นไป (มือใหม่อ่านก่อน):

  • component = function ที่คืน JSX → ใช้สร้าง UI
  • props = "พารามิเตอร์" ของ component (ข้อมูลที่ parent ส่งให้ child)
  • state = ข้อมูลภายใน component ที่เปลี่ยนได้ + ทำให้ component re-render เมื่อเปลี่ยน
  • hook = function ที่ขึ้นต้นด้วย use (เช่น useState, useEffect) — เป็น "ช่อง" ที่ component ใช้ผูกกับระบบของ React (รายละเอียดบท 2)
  • render = React สร้าง DOM ออกหน้าจอจาก component + state ปัจจุบัน
  • re-render = render ใหม่ — เกิดทุกครั้งที่ state/props เปลี่ยน

JS/TS syntax ที่จะเจอ (ทบทวนจากบท 0 ถ้าลืม):

  • { name, age } ใน argument = destructuring → ดึง field จาก object เป็นตัวแปร
  • : GreetingProps หลัง argument = TypeScript type annotation (บอกชนิดของ argument)
  • () => ... = arrow function (function แบบสั้น)
  • ...obj ใน { ...user, age: 26 } = spread (กระจาย field ของ object เดิม)
tsx
function Greeting() {
    return <h1>Hello, World!</h1>;
}

// ใช้
<Greeting />

กฎ: ชื่อ component ขึ้นต้นด้วยตัวใหญ่ — <greeting /> จะถูกตีความเป็น HTML tag

5.1 Props — ส่งข้อมูลเข้า component

tsx
type GreetingProps = {
    name: string;
    age: number;
};

function Greeting({ name, age }: GreetingProps) {
    return <p>Hello, {name}! You are {age} years old.</p>;
}

// ใช้
<Greeting name="Anna" age={25} />
<Greeting name="Ben" age={30} />
  • name="Anna" — string ใช้ ""
  • age={25} — non-string ใช้ {}

5.2 Children prop

tsx
type CardProps = {
    title: string;
    children: React.ReactNode;  // React.ReactNode = type ที่หมายถึง "อะไรก็ได้ที่ React render เป็น UI ได้" (string, number, JSX, array ของ JSX, null, ...)
};

function Card({ title, children }: CardProps) {
    return (
        <div className="card">
            <h2>{title}</h2>
            <div>{children}</div>
        </div>
    );
}

// ใช้
<Card title="User Profile">
    <p>Name: Anna</p>
    <p>Age: 25</p>
</Card>

children = ทุกอย่างที่อยู่ระหว่าง <Card>...</Card>

5.3 Props เป็น read-only

tsx
function Greeting({ name }: { name: string }) {
    name = "Hacked";    // ❌ อย่าทำ — props ห้ามแก้
    return <p>{name}</p>;
}

โค้ดนี้รันได้ ไม่มี error ทันที แต่ผลลัพธ์ไม่ sync กับ parent (parent ไม่รู้ว่าค่าถูกเปลี่ยน) และงงตอน re-render (ค่าอาจเด้งกลับเป็นของเดิมจาก parent) — เป็น anti-pattern ไม่ใช่ syntax error

ถ้าจะเปลี่ยน → ใช้ state


6. State — ข้อมูลที่ component เก็บ + เปลี่ยนได้

tsx
import { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0);
    
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>+</button>
            <button onClick={() => setCount(count - 1)}>-</button>
            <button onClick={() => setCount(0)}>Reset</button>
        </div>
    );
}

6.1 useState — anatomy

tsx
const [count, setCount] = useState(0);
//     ^      ^                    ^
//     |      |                    initial value
//     |      setter — เรียกเพื่อเปลี่ยน
//     getter — อ่านค่าปัจจุบัน

useState return array 2 ตัว — destructure เป็น [value, setter]

6.2 ⚠️ ห้ามแก้ state ตรง ๆ

tsx
// ❌ wrong
count = 5;
count++;

// ✅ right
setCount(5);
setCount(count + 1);

6.3 State แบบ functional update

tsx
setCount(count + 1);              // ใช้ value ปัจจุบัน
setCount(prev => prev + 1);       // ใช้ value ล่าสุด (ปลอดภัยกว่า)

รายละเอียดทำไม functional update ปลอดภัยกว่า (เกี่ยวกับ closure) → บท 2

ถ้าเรียก setCount หลายครั้งใน event เดียว — ใช้ functional ปลอดภัยกว่า:

tsx
// ❌ count เพิ่มแค่ 1 (ทั้งสามครั้งใช้ count เดิม)
function handle() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
}

// ✅ count เพิ่ม 3
function handle() {
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
}

6.4 State แบบ object/array — ต้อง immutable

tsx
const [user, setUser] = useState({ name: "Anna", age: 25 });

// ❌ wrong — mutate
user.age = 26;
setUser(user);     // React ไม่เห็นว่าเปลี่ยน

// ✅ create new object
setUser({ ...user, age: 26 });
setUser(prev => ({ ...prev, age: prev.age + 1 }));
tsx
const [items, setItems] = useState<string[]>([]);

// ❌ wrong
items.push("new");
setItems(items);

// ✅
setItems([...items, "new"]);
setItems(prev => [...prev, "new"]);

// ลบ
setItems(prev => prev.filter(x => x !== "old"));

// แก้
setItems(prev => prev.map(x => x === "old" ? "new" : x));

7. Event Handling

tsx
<button onClick={() => alert("clicked")}>Click</button>

<input
    onChange={e => setText(e.target.value)}
    onFocus={() => console.log("focused")}   // เมื่อ user คลิกเข้ามาในช่อง
    onBlur={() => console.log("blurred")}    // เมื่อ user คลิกออกจากช่อง
/>

<form onSubmit={e => {
    e.preventDefault();      // กัน form submit ปกติ
    handleSubmit();
}}>

Event handler:

  • ใช้ camelCase: onClick, onChange, onSubmit
  • ค่าเป็น function (ไม่ใช่ string)
  • รับ event object เป็น argument

Event types ใน TS

tsx
function Button() {
    // React.MouseEvent<HTMLButtonElement> = type ของ event object ที่มาจาก <button> เวลาถูกคลิก
    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
        console.log(e.currentTarget);
    };
    return <button onClick={handleClick}>Click</button>;
}

8. TODO List — combine ทุกอย่าง

ปิดท้ายบทด้วยตัวอย่างที่รวมทุกอย่างที่เรียนมา — TODO list ที่ใช้ทั้ง state, event handler, การ render list และ conditional rendering ลองอ่านโค้ดนี้แล้วจะเห็นว่าแต่ละชิ้นที่เรียนแยกกันประกอบเป็น app จริงได้อย่างไร:

tsx
import { useState } from 'react';

type Todo = {
    id: number;
    text: string;
    done: boolean;
};

function TodoApp() {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [input, setInput] = useState("");
    
    const addTodo = () => {
        if (input.trim() === "") return;
        const newTodo: Todo = {
            id: Date.now(),   // พอสำหรับตัวอย่างนี้ — production ควรใช้ crypto.randomUUID() หรือ nanoid แทน
            text: input,
            done: false,
        };
        setTodos(prev => [...prev, newTodo]);
        setInput("");
    };
    
    const toggleTodo = (id: number) => {
        setTodos(prev => prev.map(t =>
            t.id === id ? { ...t, done: !t.done } : t
        ));
    };
    
    const deleteTodo = (id: number) => {
        setTodos(prev => prev.filter(t => t.id !== id));
    };
    
    return (
        <div>
            <h1>Todo List</h1>
            <div>
                <input
                    value={input}
                    onChange={e => setInput(e.target.value)}
                    onKeyDown={e => e.key === "Enter" && addTodo()}
                    placeholder="สิ่งที่ต้องทำ..."
                />
                <button onClick={addTodo}>Add</button>
            </div>
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input
                            type="checkbox"
                            checked={todo.done}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        <span style={{
                            textDecoration: todo.done ? 'line-through' : 'none'
                        }}>
                            {todo.text}
                        </span>
                        <button onClick={() => deleteTodo(todo.id)}>×</button>
                    </li>
                ))}
            </ul>
            <p>Total: {todos.length}, Done: {todos.filter(t => t.done).length}</p>
        </div>
    );
}

export default TodoApp;

แทน App.tsx ด้วยอันนี้ → save → Vite hot reload (โหลดหน้าเว็บใหม่อัตโนมัติเมื่อ save ไฟล์ ไม่ต้อง refresh browser เอง) → ลองเล่น


9. แยก Component

โค้ดบนทำได้ แต่ component ยาว → แตก:

📝 ขั้นตอนก่อนสร้างไฟล์ใหม่:

  1. สร้าง folder ชื่อ components ใน src/ ก่อน (คลิกขวา > New Folder ใน editor หรือรัน mkdir src/components ใน terminal)
  2. สร้างไฟล์ใหม่ใน folder นั้น
tsx
// components/TodoItem.tsx
// นิยาม Todo type ในไฟล์นี้ด้วย หรือ import จาก App.tsx ถ้า export ออกมาแล้ว
type Todo = { id: number; text: string; done: boolean };

type Props = {
    todo: Todo;
    onToggle: (id: number) => void;
    onDelete: (id: number) => void;
};

function TodoItem({ todo, onToggle, onDelete }: Props) {
    return (
        <li>
            <input
                type="checkbox"
                checked={todo.done}
                onChange={() => onToggle(todo.id)}
            />
            <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
                {todo.text}
            </span>
            <button onClick={() => onDelete(todo.id)}>×</button>
        </li>
    );
}

export default TodoItem;
tsx
// components/TodoInput.tsx
import { useState } from 'react';

type Props = {
    onAdd: (text: string) => void;
};

function TodoInput({ onAdd }: Props) {
    const [input, setInput] = useState("");
    
    const submit = () => {
        if (input.trim() === "") return;
        onAdd(input);
        setInput("");
    };
    
    return (
        <div>
            <input
                value={input}
                onChange={e => setInput(e.target.value)}
                onKeyDown={e => e.key === "Enter" && submit()}
                placeholder="สิ่งที่ต้องทำ..."
            />
            <button onClick={submit}>Add</button>
        </div>
    );
}

export default TodoInput;
tsx
// App.tsx
import { useState } from 'react';
import TodoItem from './components/TodoItem';
import TodoInput from './components/TodoInput';

type Todo = { id: number; text: string; done: boolean };

function App() {
    const [todos, setTodos] = useState<Todo[]>([]);
    
    const addTodo = (text: string) =>
        setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
    
    const toggleTodo = (id: number) =>
        setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
    
    const deleteTodo = (id: number) =>
        setTodos(prev => prev.filter(t => t.id !== id));
    
    return (
        <div>
            <h1>Todo List</h1>
            <TodoInput onAdd={addTodo} />
            <ul>
                {todos.map(todo => (
                    <TodoItem
                        key={todo.id}
                        todo={todo}
                        onToggle={toggleTodo}
                        onDelete={deleteTodo}
                    />
                ))}
            </ul>
        </div>
    );
}

export default App;

ดูแล้ว:

  • State อยู่ที่ App (parent) — เรียกว่า "lift state up" (ยก state ขึ้นไปไว้ที่ parent เพื่อให้ลูก ๆ ใช้ร่วมกันได้ — ถ้าอยู่ที่ child แต่ละตัว, share ข้อมูลกันไม่ได้)
  • ส่ง data ลง child ผ่าน props
  • ส่ง action ขึ้น parent ผ่าน callback props

10. กฎ React ที่ต้องจำ

10.1 ขึ้นต้นด้วยตัวใหญ่เสมอ

tsx
function MyComponent() { ... }    // ✅
function myComponent() { ... }    // ❌ React คิดว่าเป็น HTML tag

10.2 Return JSX 1 อันเดียว

ใช้ fragment <></> ถ้าไม่อยาก wrap div

10.3 ใช้ key prop ใน list

jsx
items.map(i => <Item key={i.id} ... />)

10.4 ห้าม mutate state

ใช้ spread (...) สร้าง array/object ใหม่ → setState

10.5 ห้ามเรียก hook ใน condition/loop

tsx
// ❌ wrong — เรียก hook ใน if
function Bad() {
    if (some) {
        const [x, setX] = useState(0);   // ❌
    }
}

// ✅ right — เรียก hook ที่ top level ของ component (ใน body ของ function โดยตรง)
function Good() {
    const [x, setX] = useState(0);       // ✅
    if (some) {
        // ใช้ x / setX ตรงนี้ได้
    }
}

top level = ใน body ของ component โดยตรง (ไม่ใช่ใน if / for / try / function ซ้อน) — hook ต้องเรียกที่ top level เสมอ (เดี๋ยวบทถัดไปอธิบายทำไม)

10.6 ห้ามแก้ DOM ตรง ๆ

ใช้ React state แทน — ปล่อย React จัดการ DOM


11. CSS — มีหลายวิธี

11.1 External CSS file

tsx
import './App.css';
<div className="card">...</div>

11.2 Inline

tsx
<div style={{ color: 'red', padding: 10 }}>...</div>

11.3 CSS Modules

tsx
import styles from './App.module.css';
<div className={styles.card}>...</div>
tsx
<div className="bg-blue-500 text-white p-4 rounded">...</div>

— class utility ที่ใช้ทันที ไม่ต้องเขียน CSS file

⚠️ ต้องลง Tailwind ก่อน (วิธีติดตั้งดูบท 6) — ใส่ class พวกนี้ใน plain CSS template ของ Vite (ที่ยังไม่ได้ลง Tailwind) จะไม่มีผล

11.5 styled-components / Emotion (CSS-in-JS — เขียน CSS อยู่ในไฟล์ JS โดยตรง)

ใช้น้อยลงในปี 2026 — Tailwind ครองตลาด


12. Checkpoint

🛠️ Checkpoint 1.1 — ปรับ Todo

เพิ่ม feature:

  • ปุ่ม "Clear completed" ลบเฉพาะ todo ที่ done
  • Filter: All / Active / Done

💡 ส่วน "จำ todos ใน localStorage (เปิด/ปิด browser แล้วยังอยู่)" ต้องใช้ useEffect ซึ่งจะสอนในบท 2 — เก็บไว้ทำต่อหลังเรียนบท 2 (จะมี custom hook useLocalStorage ในบท 2 §9)

📖 เฉลย (key parts)
tsx
const [filter, setFilter] = useState<'all' | 'active' | 'done'>('all');

const filtered = todos.filter(t => {
    if (filter === 'active') return !t.done;
    if (filter === 'done') return t.done;
    return true;
});

const clearCompleted = () => setTodos(prev => prev.filter(t => !t.done));

🛠️ Checkpoint 1.2 — Calculator

สร้าง simple calculator:

  • input 2 ตัว (number)
  • เลือก operation (+, -, *, /)
  • แสดงผล
📖 เฉลย
tsx
function Calculator() {
    const [a, setA] = useState(0);
    const [b, setB] = useState(0);
    const [op, setOp] = useState<'+' | '-' | '*' | '/'>('+');
    
    const result = (() => {
        switch (op) {
            case '+': return a + b;
            case '-': return a - b;
            case '*': return a * b;
            case '/': return b !== 0 ? a / b : 'error';
            // ไม่ต้องมี default — TypeScript รู้ว่า op เป็น union type '+' | '-' | '*' | '/' เท่านั้น
            // เช็คครบ 4 case แล้วจึง exhaustive (ครอบคลุมทุกความเป็นไปได้) แล้ว
        }
    })();
    
    return (
        <div>
            <input type="number" value={a} onChange={e => setA(Number(e.target.value))} />
            <select value={op} onChange={e => setOp(e.target.value as '+' | '-' | '*' | '/')}>
                <option>+</option><option>-</option><option>*</option><option>/</option>
            </select>
            <input type="number" value={b} onChange={e => setB(Number(e.target.value))} />
            <span> = {result}</span>
        </div>
    );
}

13. สรุปบท

✅ Vite + React + TypeScript = stack เริ่มต้น ✅ Component = function ที่ return JSX (ตัวใหญ่ขึ้นต้น) ✅ JSX: className, camelCase attribute, { } ใส่ JS, fragment <></> ✅ Props: ส่ง data จาก parent → child (read-only) ✅ State: const [v, setV] = useState(initial) — เปลี่ยนผ่าน setter เท่านั้น ✅ Immutable updates: spread [...prev, x], {...prev, key: v} ✅ Event: onClick, onChange — function ไม่ใช่ string ✅ List + key prop ✅ Lift state up — state อยู่ที่ parent, ส่ง callback ลง child


14. ต่อจากนี้

บทที่จะตามมา (ดูสารบัญเต็มได้ที่ README):

หัวข้อสอนอะไร
02 — Hooks ลึกuseEffect, useMemo, useCallback, useRef, useContext, custom hooks
03 — Form + Validationcontrolled component, React Hook Form, validation
04 — Data fetchingfetch, useEffect pattern, TanStack Query
05 — RoutingReact Router — multi-page SPA
06 — Styling + UI LibrariesTailwind CSS, CSS Modules, shadcn/ui
07 — State managementZustand, Redux Toolkit, Context
08 — TestingVitest, React Testing Library

ตอนนี้ฝึก checkpoint บทนี้ให้คล่อง — เป็นพื้นฐานทุกบทต่อ


← กลับสารบัญ React Book | ← กลับสารบัญหลัก | → บทที่ 2: Hooks เชิงลึก