สอนเขียนเว็บด้วย React.js ปี 2023 (Intro to React) ตอนที่ 2

React Jun 29, 2023

มาต่อกันที่ ตอนที่ 2 นะครับ ในตอนนี้จะเป็นเรื่องของการจัดการ Form, การดึงข้อมูลจาก API ด้วยการใช้ useEffect()  ร่วมกับ fetch หรือ axios รวมถึงการจัดการ loading state และ การทำ custom hooks แบบง่ายๆครับ

เนื้อหาสอนเขียนเว็บ React.js มีทั้งหมด 3 ตอนนะครับ

สำหรับตอนนี้ จะมี Source Code เพื่อลองเปรียบเทียบ และทำความเข้าใจเพิ่มเติมครับ กด Link เพื่อดู Source Code ใน Github ได้เลย

Source Code

1. 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() คือ

  1. ทำการสร้างตัวแปรขึ้นมาเก็บ ref ก่อน
  2. กำหนด ref ที่ตัว <input> ให้ reference ไปที่ตัวแปรที่เราสร้างจากข้อ 1
  3. ใน 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

แต่ ตัว 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} />

ตัวอย่างที่หลายๆ คนลืม คือ 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

<input> – React
The library for web and native user interfaces

นอกจากนี้ เวลาจัดการ Form จริงๆ ก็จะมี library ที่คนนิยมใช้กัน ก็คือ react-hook-form ครับ สามารถไปอ่านเพิ่มเติมได้

GitHub - react-hook-form/react-hook-form: 📋 React Hooks for form state management and validation (Web + React Native)
📋 React Hooks for form state management and validation (Web + React Native) - GitHub - react-hook-form/react-hook-form: 📋 React Hooks for form state management and validation (Web + React Native)

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

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/

JSONPlaceholder - Free Fake REST API

ก็คือ เราจะดึงข้อมูลหลักจาก 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 เพิ่มเติม

Using the Fetch API - Web APIs | MDN
The Fetch API provides a JavaScript interface for accessing and manipulating parts of the protocol, such as requests and responses. It also provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across the network.

หรือถ้าใช้ 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

ทีนี้ ในการ 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

นอกจากนี้มี Library เกี่ยวกับ fetching ที่น่าสนใจ ลองไปดูเพิ่มเติมได้ครับ

React Hooks for Data Fetching – SWR
SWR is a React Hooks library for data fetching. SWR first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again.
TanStack Query | React Query, Solid Query, Svelte Query, Vue Query
Powerful asynchronous state management, server-state utilities and data fetching for TS/JS, React, Solid, Svelte and Vue

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

Happy Coding ❤️

Tags

Chai Phonbopit

เป็น Web Dev ทำงานมา 10 ปีหน่อยๆ ด้วยภาษา JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจ Web3, Crypto และ Blockchain เขียนบล็อกที่ https://devahoy.com