สอนเขียนเว็บด้วย React.js ปี 2023 (Intro to React) ตอนที่ 2
มาต่อกันที่ ตอนที่ 2 นะครับ ในตอนนี้จะเป็นเรื่องของการจัดการ Form, การดึงข้อมูลจาก API ด้วยการใช้ useEffect()
ร่วมกับ fetch
หรือ axios
รวมถึงการจัดการ loading state และ การทำ custom hooks แบบง่ายๆครับ
เนื้อหาสอนเขียนเว็บ React.js มีทั้งหมด 3 ตอนนะครับ
- ตอนที่ 1 - React เบื้องต้น เช่น Component, JSX, Props, State และ Events
- ตอนที่ 2 - React Form, useEffect, fetchAPI, React Hooks, Custom Hooks
- ตอนที่ 3 - React Router และการ Deployment
สำหรับตอนนี้ จะมี Source Code เพื่อลองเปรียบเทียบ และทำความเข้าใจเพิ่มเติมครับ กด Link เพื่อดู Source Code ใน Github ได้เลย
Source Code1. Form
หลักจากที่รู้เรื่อง state และ event ไปแล้ว คราวนี้มาถึงเรื่องของ form กันบ้าง ซึ่ง form น่าจะเป็นอีกหนึ่งส่วนที่หน้าเว็บ ส่วนใหญ่ต้องมี ไม่ว่าจะเป็นหน้า Login / Register หรือในระบบต่าง ที่ต้องมีการส่งข้อมูลจากผู้ใช้งาน ไปที่ server ของเรา
ใน React เราสามารถจัดการกับ Form ได้ 2 แบบคือ
1. Uncontrolled Form
โดยปกติ ใน HTML พวก <input>
, <textarea>
และ <select>
จะมี state ในตัวเอง เวลาที่ user input หรือเปลี่ยนแปลงค่า (บางที่อาจจะเรียกแบบนี้ว่า Uncontrolled Form/Input)
2. Controlled Form
ส่วนนี้คือ เราใช้วิธีการสร้าง state
ขึ้นมา เพื่อควบคุมข้อมูลของ input ผ่านพวก onChange
และการ setValue
(ที่เราได้เห็นจากตัวอย่างตอนที่ 1 หรือ Workshop ที่ทำ BMI Calculator)
Uncontrolled input
การเขียน Uncontrolled Form ก็ไม่ยากเลยครับ เราไม่ต้องไปจัดการกับ value เอง เราใช้วิธี handleSubmit
อย่างเดียวพอ และก็ดึงค่าจาก form
มาใช้เลย
ตัวอย่าง การใช้ event.currentTarget
import { FormEvent } from 'react'
const Form1 = () => {
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const target = e.currentTarget
const formData = {
username: target.username.value,
password: target.password.value,
}
console.log('Form1 -->', formData)
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Submit</button>
</form>
)
}
export default Form1
จากโค๊ดจะเห็นว่า เราใช้ onSubmit
และเรียก function handleSubmit
ในนั้น ก็สามารถเข้าถึงข้อมูลใน form ได้ผ่าน event.currentTarget.<NAME>.value
นั่นเอง
ดูโค๊ดได้ที่นี่ Form1.tsx
ตัวอย่างการใช้ React.useRef()
import { FormEvent, useRef } from 'react'
const Form2 = () => {
const usernameRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const username = usernameRef.current?.value
const password = passwordRef.current?.value
const formData = {
username,
password,
}
console.log('Form2 -->', formData)
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="username" ref={usernameRef} />
<input type="password" name="password" ref={passwordRef} />
<button type="submit">Submit</button>
</form>
)
}
export default Form2
การใช้ React.useRef()
คือ
- ทำการสร้างตัวแปรขึ้นมาเก็บ ref ก่อน
- กำหนด
ref
ที่ตัว<input>
ให้ reference ไปที่ตัวแปรที่เราสร้างจากข้อ 1 - ใน
handleSubmit
เราก็สามารถเข้าถึงref.current.value
ได้
โค๊ดตัวอย่างของ Form2.tsx
Controlled input
การเขียนแบบ controlled input คือเราแยกมาเก็บเป็น state ใน Component และใช้การ handle onChange
เพื่อเปลี่ยนค่า state ครับ ตัวอย่างเช่น
import { ChangeEvent, FormEvent, useState } from 'react'
const Form3 = () => {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = {
username,
password,
}
console.log('Form3 -->', formData)
}
const handleChangeUsername = (e: ChangeEvent<HTMLInputElement>) => {
setUsername(e.target.value)
}
const handleChangePassword = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="username" onChange={handleChangeUsername} />
<input type="password" name="password" onChange={handleChangePassword} />
<button type="submit">Submit</button>
</form>
)
}
export default Form3
- โค๊ดของ Form3.tsx
แต่ ตัว handle onChange เราจะเห็นว่ามันค่อนข้างเป็นโค๊ดที่ซ้ำๆ สมมติ เรามี input เพิ่มมาอีก 4-5ตัว เราต้องมาสร้าง handle onChange 4-5ตัว คงไม่สนุกเท่าไหร่ เราสามารถ resuse เป็น function เดียว แล้วแยกกันด้วย event.target.name
ได้ครับ
const [formData, setFormData] = useState({
username: '',
password: '',
})
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
// input
<input type="text" name="username" onChange={handleChange} />
<input type="password" name="password" onChange={handleChange} />
- โค๊ดของ Form4.tsx
ตัวอย่างที่หลายๆ คนลืม คือ controller ด้วยการกำหนด value กับ state แต่ไม่มี onChange
// 🔴 Bug: controlled input ด้วย value แต่ไม่มี onChange
<input value={something} />
ถ้าเราต้องการแค่ให้มัน initial value แต่ไม่ต้องการควบคุม state ด้วย React (ใช้ state ของ input เอง) เราก็แค่ใช้ defaultValue
ก็พอ
// ✅ uncontrolled input ด้วย defaultState
<input defaultValue={something} />
Optimizing re-rendering on every keystroke

นอกจากนี้ เวลาจัดการ Form จริงๆ ก็จะมี library ที่คนนิยมใช้กัน ก็คือ react-hook-form ครับ สามารถไปอ่านเพิ่มเติมได้
2. useEffect
useEffect
เป็น React Hook เอาไว้จัดการกับพวกข้อมูลภายนอก Component หรือเป็น function ที่เอาไว้ทำ action บางอย่างหลังจาก component render เช่น เมื่อ component render เสร็จ :
- เซ็ตค่า document title ของเว็บ
- ดึงข้อมูล API มาแสดงผล
เข้าใจกฎของ React Hook ซักเล็กน้อย
- React Hook เป็น JavaScript function ปกตินี้แหละ
- ต้อง call react hook ที่ top level เท่านั้น
- ไม่สามารถ call react hook ที่ loops, condition หรือใน function ได้
- call ได้แค่ใน React Component (function)
รูปแบบการใช้ useEffect จะเป็นแบบนี้
React.useEffect(setup, dependencies?)
โดย default, Effect จะรันหลังจาก ทุกๆ render (ตั้งแต่ render ครั้งแรก และทุกๆ ครั้งที่ re-render)
รูปแบบ useEffect()
ในการเปลี่ยนค่า document title
import { useEffect, useState } from 'react'
const UseEffect = () => {
useEffect(() => {
console.log('useEffect called')
document.title = `UseEffect Example`
})
console.log('UseEffect render...')
return (
<div>
<h1>UseEffect Example</h1>
</div>
)
}
export default UseEffect
ตัว useEffect()
จะรับ parameter แรกเป็น function และตัวที่สองคือ dependencies ตัวอย่างด้านบน ถ้าเราไม่กำหนด dependencies (เช่น []
) useEffect()
นี้มันก็จะ call ครั้งแรกที่ mount และทุกๆ re-render เวลา state เปลี่ยน นั่นเอง
เราสามารถกำหนด ให้ useEffect()
รันครั้งเดียว แค่ตอน component mounted ได้ แบบนี้
useEffect(() => {
console.log('useEffect called')
document.title = `UseEffect Example`
}, [])
ค่าข้างใน dependencies จะเป็นตัวเอาไว้เช็คว่า useEffect()
จะถูกเรียกหรือไม่ ถ้าค่าใน dependencies เปลี่ยน useEffect()
ถึงจะถูกเรียก (ในที่นี้เราใช้ empty array)
ตัวอย่าง เช่น เราให้มัน set document title เป็นค่า count
ที่ User ทำการ click แบบนี้
import { useEffect, useState } from 'react'
const UseEffect = () => {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('useEffect called')
document.title = `Click ${count} times`
}, [count])
console.log('UseEffect render...')
return (
<div>
<h1>UseEffect Example</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
export default UseEffect
- ตัวอย่างโค๊ด UseEffect.tsx
Infinity Loop
ระวังเรื่อง infinity loop เนื่องจากว่าทุกการ update state ตัว Component จะทำการ re-render เมื่อ state เปลี่ยน
// ❌ ห้ามทำแบบนี้เด็ดขาด!
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count + 1)
}, [count])
// ✅ อย่างน้อย ใช้ empty array เป็น default
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count + 1)
}, []) // <--- เพิ่มตรงนี้
3. Fetch API
ต่อมาเรื่องของการดึงข้อมูลจาก API แน่นอน น่าจะเป็นพื้นฐานที่เกือบทุกเว็บจะต้องใช้ เพราะเราเขียน React เป็นแค่ UI ก็ต้องหาวิธีการนำ Data มาแสดงให้ได้ มาดูตัวอย่างการดึงข้อมูลกันนะครับ เราจะใช้ fetch
คู่กับ useEffect()
ก่อนหน้านี้
โดยตัว API เราจะใช้เว็บฟรี เว็บนี้ - https://jsonplaceholder.typicode.com/

ก็คือ เราจะดึงข้อมูลหลักจาก Component render แล้ว นั่นเอง ตัวอย่าง ทำการดึงข้อมูล posts จาก API
useEffect(() => {
const url = 'https://jsonplaceholder.typicode.com/posts'
fetch(url)
.then((res) => res.json())
.then((data) => {
console.log('data -->', data)
})
}, [])
อ่าน fetch เพิ่มเติม

หรือถ้าใช้ axios ก็แทนที่ fetch (ข้อดีของ fetch คือเป็น native api ไม่ต้องติดตั้งอะไรเพิ่มเติม แต่ถ้าเป็น axios เราก็ต้องติดตั้ง dependencie ก่อน
npm install axios
ตัวอย่างการใช้ useEffect
+ axios
import axios from 'axios'
useEffect(() => {
const url = 'https://jsonplaceholder.typicode.com/posts'
axios.get(url).then((res) => {
console.log('axios res -->', res)
})
}, [])
เปลี่ยนมาใช้แบบ async/await แทน
import axios from 'axios'
const fetchData = async (url: string) => {
const res = await fetch(url)
const data = await res.json()
// หรือแบบ axios
// const res = await axios.get(url)
console.log('fetch async -->', data)
}
useEffect(() => {
const url = 'https://jsonplaceholder.typicode.com/posts'
fetchData(url)
}, [])
การ fetch ข้อมูล Posts เมื่อได้ข้อมูล เราก็จะนำมาเก็บใน state เพื่อไว้แสดงผลข้อมูล
เพิ่ม state สำหรับเก็บ Posts
const [posts, setPosts] = useState([])
จากนั้น เมื่อ fetch ข้อมูลเสร็จ ต่อจาก console.log
เราก็ทำการ setPosts
ค่าที่ได้ลง state จากนั้นก็ loop ข้อมูลด้วย map()
และเนื่องจาก เรารู้ว่า response จาก API จะมีหน้าตาเป็นแบบไหน เราก็กำหนด interface ให้มันก่อน แบบนี้
interface Post {
userId: number
id: number
title: string
body: string
}
ส่วน useState
ก็แก้ไขเป็นแบบนี้ เพราะถ้าเราไม่กำหนด ตัว TypeScript จะไม่รู้ว่า posts
มันคือชนิดอะไร
const [posts, setPosts] = useState<Post[]>([])
ตัวอย่าง โค๊ดที่ได้ก็จะมีหน้าตาแบบนี้
import { useEffect, useState } from 'react'
import axios from 'axios'
interface Post {
userId: number
id: number
title: string
body: string
}
const FetchAPI = () => {
const [posts, setPosts] = useState<Post[]>([])
const fetchData = async (url: string) => {
const res = await fetch(url)
const data = await res.json()
// หรือ axios
// const res = await axios.get(url)
// setPosts(res.data)
console.log('fetch async -->', data)
setPosts(data)
}
useEffect(() => {
const url = 'https://jsonplaceholder.typicode.com/posts'
// fetch(url)
// .then((res) => res.json())
// .then((data) => {
// console.log('fetch data -->', data)
// setPosts(data)
// })
// axios.get(url).then((res) => {
// console.log('axios res -->', res)
// setPosts(res.data)
// })
fetchData(url)
}, [])
return (
<div>
<h1>Fetch API Example</h1>
<h3>Users</h3>
<ul>
{posts.map((post) => {
return (
<li key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</li>
)
})}
</ul>
</div>
)
}
export default FetchAPI
- ตัวอย่างโค๊ด FetchAPI.tsx
ทีนี้ ในการ fetch ข้อมูลจาก API ส่วนใหญ่ เราจะมีจังหวะ รอข้อมูลใช่มั้ย จริงๆ เราควรมี state สำหรับ loading หรือบางครั้ง API มี error เราก็ควรจะมี error เช่นกัน ตัวอย่างเช่น เพิ่ม loading state และจัดการกรณี error โดยการเพิ่ม state ลงไปเป็นแบบนี้
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
ทีนี้ ตอนที่เรากำลัง fetch ข้อมูล เราก็ set state ให้ isLoading
เป็น true
และถ้า fetch เสร็จเรียบร้อยแล้ว ก็ set เป็น false
ถ้ากรณีที่ fetch แล้วมี error เราก็ setError
ให้มันแบบนี้
const fetchUsers = async () => {
try {
setIsLoading(true) // <--- set loading = true
const res = await axios.get<User[]>(url)
setUsers(res.data)
setIsLoading(false) // <--- set loading = false
} catch (error: any) {
setError(error.response?.message || error.message)
}
}
ทีนี้ส่วน markup เวลา render เราก็เพิ่ม condition rendering ไปด้วย ถ้า isLoading
อยู่ ก็ให้โชว์ว่ากำลังโหลด ถ้าโหลดเสร็จ ก็แสดง users list ปกติ
return (
<div className="users">
{isLoading ? <p>Loading....</p> : (
{users.map(user => {
return (
<div className="user" key={user.id}>
<h2></h2>
<p></p>
</div>
)
})}
)}
</div>
หรือจะดักเงื่อนไขก่อน return
เลยก็ได้เช่น
if (isLoading) return <p>Loading...</p>
ตัวอย่าง Fetch API แบบมี loading และ handle error:
import { useEffect, useState } from 'react'
import axios from 'axios'
interface Post {
userId: number
id: number
title: string
body: string
}
const FetchAPI2 = () => {
const [posts, setPosts] = useState<Post[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchData = async (url: string) => {
try {
setIsLoading(true)
const res = await axios.get(url)
setPosts(res.data)
} catch (error: any) {
setError(error.response?.data?.message || error.message)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
const url = 'https://jsonplaceholder.typicode.com/posts'
fetchData(url)
}, [])
if (isLoading) return <div>Loading...</div>
if (error) return <div>{error}</div>
return (
<div>
<h1>Fetch API Example</h1>
<h3>Users</h3>
<ul>
{posts.map((post) => {
return (
<li key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</li>
)
})}
</ul>
</div>
)
}
export default FetchAPI2
- ตัวอย่างโค๊ด FetchAPI2.tsx
นอกจากนี้มี Library เกี่ยวกับ fetching ที่น่าสนใจ ลองไปดูเพิ่มเติมได้ครับ

จบไปแล้วสำหรับตอนที่ 2 รอต่อที่ตอนสุดท้าย ตอนที่ 3 นะครับ เรื่องของ React Router, State Management และการ Deployment ครับ
Happy Coding ❤️