React Hooks คืออะไร? + มาลองหัดใช้กันดีกว่า

วันนี้จะมาพูดถึงเรื่อง Hooks กันเนาะ เนื่องจากได้ลองอ่าน Introducing Hooks — React เหตุเพราะไปเห็น Repo ตัวนึง ใน Github ว่าทำไมมันขึ้นมาเป็น Trending ชื่อ react-use มีอะไรน่าสนใจนะ ซึ่งก็อ่านผ่านๆและไม่ได้สนใจอะไรมาก จนกระทั่งมาอ๋อๆๆ ตอนที่เห็น Blog เรื่อง React Hooks นี่แหละ พบว่ามันมีจุดน่าสนใจเป็นอย่างยิ่ง จึงลองอ่าน ทำความเข้าใจ และก็มาบล็อกบอกเล่าเรื่องราวในการทดลองเล่น React Hook แถมเป็นการทบทวนตัวเองไปในตัวด้วย ใครผ่านมาอ่านก็มาช่วยคอมเม้น เสนอแนะได้เนาะ
Hook คืออะไร?
Hook คือ feature ใหม่ที่เพิ่งมีใน React v16.7.0-alpha ซึ่ง Dan Abramov ได้พูดถึงไว้ใน Video นี้ครับ
Link Youtube : https://www.youtube.com/watch?v=V-QO-KO90iQ
จริงๆแล้ว Hook มันก็คือ function ที่ให้เราสามารถใช้ react features ได้ เช่น ให้เราสามารถเรียกใช้ state ได้ โดยที่ไม่ต้องใช้การ implement Class แล้ว
หากใครที่เขียน React มา แล้วใช้แบบ Stateless Component อยู่ๆวันดีคืนดี นึกได้ว่า Component นี้มันต้องใช้ lifecycle ใช้ state ด้วยวุ้ย ก็ต้องนั่งเปลี่ยนจาก function component มาเป็น Class และ extends Component แต่พอเป็น Hook เราไม่ต้องมาเปลี่ยนคลาสแล้ว ก็ใช้ function component ได้เลย
ซึ่งหลายๆข้อทำให้เกิด React Hook ขึ้นมา เช่น
- Component มีขนาดใหญ่ ยากต่อการ refactor และ test
- ต้องทำ Logic หรือ Lifecycle ซ้ำๆ ระหว่างแต่ Component
- Complex Pattern พวก render props หรือ HOC
ซึ่งก็เลยทำให้ Hook มันเกิดมาเพื่อแก้ปัญหา เพื่อ reuse ได้นั่นเอง อ้างอิงจาก Blog ของ Dan Abramov นี้ครับ
กฎการใช้ Hooks
- ต้องเรียก Hook ที่ส่วน Top Level ของ function เท่านั้น
- ต้องเรียก Hook ภายใน React function เท่านั้น (function components)
Class Component แบบปกติ
ก่อนไปเริ่ม Hooks มาย้อนทำ Class Component แบบพื้นฐาน ในการ handle counter กัน ก็จะหน้าตาประมาณนี้
ตัวอย่าง Class Component ปกติ ที่เวลา เราต้องการให้ Component มี State หรือบางที ก็อาจจะมีการ fetch ข้อมูลด้วย componentDidMount()
ก็จะเป็นลักษณะแบบนี้
import React, { Component } from 'react'class ExampleHook extends Component { constructor(props) { super(props) this.state = { count: 0 } }
componentDidMount() { // fetch some data fetchData() }
handleClick = () => { this.setState((state) => ({ count: state.count + 1 })) }
render() { const { count } = this.state return ( <div> <p>You clicked {count} times</p> <button onClick={this.handleClick}>Click me</button> </div> ) }}
ซึ่งวิธีทั่วๆไป ก็คือเราทำการสร้าง Class จากนั้น ก็กำหนด this.state = {}
ขึ้นมา พร้อมทั้ง ทำ handle function เพื่อเอาไว้ handle onclick
เรียนรู้ Hooks
มาลองเรียนรู้หน้าตาเจ้า Hooks กันดีกว่า ว่ามันจะมีหน้าตาเป็นยังไง และวิธีการเรียกใช้แบบไหนบ้าง
useState
ตัวอย่างแรกของ Hooks ก็คือ State Hook ที่ชื่อว่า useState
import React, { useState } from 'react'function ExampleHook() { const [count, setCount] = useState(0)
render() { return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }}
useState
คือ Hook โดยเราเรียกมันภายใน react function componentuseState
return ค่าเป็น array ตัวแรกคือ ชื่อ state และ ตัวสองคือชื่อ function- เราใช้ array destructuring เพื่อกำหนด ชื่อ state และ function ที่ return จาก
useState
กรณีที่เราต้องการใช้ หลายๆ state ก็ทำได้ เช่น
function ExampleWithManyStates() { // Declare multiple state variables! const [age, setAge] = useState(42) const [fruit, setFruit] = useState('banana') const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]) // ...}
useEffect
ต่อมา useEffect
เป็น Hook ที่มี side effect ก็คือมีปัจจัยภายนอกเข้ามาเกี่ยวข้องกับตัว function component เช่น การแก้ไขพวก window, title หรือเรียก lifecycle ของ React เช่น componentDidMount
ตัวอย่างการใช้ useEffect
import { useState, useEffect } from 'react'
function Example() { const [count, setCount] = useState(0)
useEffect(() => { document.title = `You clicked ${count} times` })
return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> )}
useEffect()
: เป็น function ที่รับ function เป็น argument ตัวอย่างด้านบน คือ รับ function ที่ทำการเปลี่ยนค่าdocument.title
useEffect()
: ถูกรันทุกๆครั้งที่มีการ render รวมถึงการ render ครั้งแรก (ทุกครั้งที่มีการอัพเดทนั่นเอง)
มาลองเล่น Hooks กันดีกว่า
เริ่มแรก จำเป็นต้อง ติดตั้ง React เวอร์ชั่น v16.7.0-alpha ขึ้นไปครับ ถ้าเราติดตั้งแบบ yarn add react
มันจะเอาตัว stable ที่ไม่ใช่ alpha ครับ
มาเริ่มต้นด้วยการสร้างโปรเจ็คด้วย Create React App และติดตั้ง React เวอร์ชั่นล่าสุดกันครับ
npx create-react-app hello-hookscd hello-hooks
จากนั้นที่ไฟล์ package.json
ให้แก้เวอร์ชัน React เป็น ^16.7.0-alpha.2
"dependencies": { "react": "^16.7.0-alpha.2", "react-dom": "^16.7.0-alpha.2", "react-scripts": "2.1.1"}
Install dependencies ใหม่ซะ
yarn install
จากนั้นเปิดไฟล์ src/App.js
และใช้ useState()
แบบนี้
import React, { useState } from 'react'import logo from './logo.svg'import './App.css'
const App = () => { const [count, setCount] = useState(0)
return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p>
<p>You click {count} times</p> <button style={{ padding: '8px 16px', borderRadius: 4, fontSize: '1.25rem' }} onClick={() => setCount(count + 1)} > Click me </button> </header> </div> )}
export default App
เราจะได้หน้าจอ และมีปุ่ม Button สำหรับ setState ด้วยการใช้ useState()
นั่นเอง ทีนี้เราก็สามารถใช้ Hooks ได้แล้ว
ต่อมาลองทำ useEffect()
เพื่อ fetch ข้อมูลจาก Random Cat API แทน การใช้ componentDidMount()
กันดูบ้าง
const [cat, setCat] = useState({})
const randomCat = () => axios.get('https://aws.random.cat/meow')
useEffect(() => { randomCat().then((response) => { setCat(response.data) })})
// ใส่ log เพื่อดูว่ามัน render ยังไงconsole.log('render >>>')
return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p>
<p>You click {state.count} times</p>
<button style={{ padding: '8px 16px', borderRadius: 4, fontSize: '1.25rem' }} onClick={() => { dispatch({ type: 'COUNTER_CLICK', payload: state.count + 1 }) }} > Click me </button>
<p> <img src={state.cat.file} alt="Cat" width="256" /> </p> </header> </div>)
ทดลองดูหน้าเว็บ แต่ๆ
เว็บเรามันทำการ render รัวๆเลย ก็เพราะว่าโดยปกติ useEffect()
มันถูกเรียกทุกๆครั้งที่ทำการ render ถ้าเราจะไม่ให้มัน รันใหม่ ให้เฉพาะตอน mount และ unmont เราก็ต้องส่ง argument ตัวที่สองเป็น array ไปให้มันด้วย เช่น ส่ง empty array ไปแบบนี้
หรือจะใส่ ค่า state ที่เราไว้เช็คว่า ค่าใน array มีการเปลียนแปลง มันก็จะเรียก
useEffect
อีกครั้ง เช่นuseEffect(fn, [someValue])
const fn = () => { randomCat().then()}
useEffect(fn, [])
สุดท้ายไฟล์ src/App.js
จะได้แบบนี้
import React, { useState, useEffect } from 'react'import axios from 'axios'import logo from './logo.svg'import './App.css'
const randomCat = () => axios.get('https://aws.random.cat/meow')
const App = () => { const [count, setCount] = useState(0) const [cat, setCat] = useState({})
useEffect(() => { randomCat().then((response) => { setCat(response.data) }) }, [])
return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p>
<p>You click {count} times</p>
<button style={{ padding: '8px 16px', borderRadius: 4, fontSize: '1.25rem' }} onClick={() => setCount(count + 1)} > Click me </button>
<p> <img src={cat.file} alt="Meow" width="256" /> </p> </header> </div> )}
export default App
useReducer
แถมครับ นอกจากนี้ ตัว Hooks ยังมี useReducer
มาให้เรา สำหรับคนที่เขียน Redux มา ก็คงคุ้นเคยดีอยู่แล้ว นั่นคือ เราสามารถใช้ Redux ใน Hooks ได้เลย ตัว Reducers ก็ไม่แตกต่างจาก Redux ครับ
หากใครไม่เคยเขียน Redux แนะนำอ่านบทความนี้เพิ่มเติมเนอะ Redux คืออะไร? + เริ่มต้นเรียนรู้ Redux ร่วมกับ React กันดีกว่า
วิธีใช้งานก็ประมาณนี้ สร้าง reducers ของเราขึ้นมา
const initialState = { isFetching: false, cat: {}}
const reducer = (state, { type, payload }) => { switch (type) { case 'FETCH_CAT_PENDING': return { ...state, isFetching: true } case 'FETCH_CAT_SUCCESS': return { ...state, isFetching: false, cat: payload } default: return state }}
// การใช้การ ก็คือเรียก useReducer ด้วย function reducer และ initiState เป็น argument ครับ
const [state, dispatch] = useReducer(reducer, initialState)
useReducer
: รับ reducer และ initiState เป็น argumentuseReducer
: return ค่ากลับมาเป็น state และ dispatch ครับ
โดยเราลองแก้ไขตัว src/App.js
ให้ใช้ state ด้วย useReducer
แทน useState
ดีกว่า
import React, { useReducer, useEffect } from 'react'import axios from 'axios'import logo from './logo.svg'import './App.css'
const fetchCat = () => axios.get('https://aws.random.cat/meow')
const initialState = { isFetching: false, cat: {}, count: 0}
const reducer = (state, { type, payload }) => { switch (type) { case 'FETCH_CAT_PENDING': return { ...state, isFetching: true } case 'FETCH_CAT_SUCCESS': return { ...state, isFetching: false, cat: payload } case 'COUNTER_CLICK': return { ...state, isFetching: false, count: payload } default: return state }}
const App = () => { const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => { dispatch({ type: 'FETCH_CAT_PENDING' })
fetchCat().then((response) => { dispatch({ type: 'FETCH_CAT_SUCCESS', payload: response.data }) }) }, [])
if (state.isFetching) { return <p>Loading....</p> }
return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p>
<p>You click {state.count} times</p>
<button style={{ padding: '8px 16px', borderRadius: 4, fontSize: '1.25rem' }} onClick={() => { dispatch({ type: 'COUNTER_CLICK', payload: state.count + 1 }) }} > Click me </button>
<p> <img src={state.cat.file} alt="Cat" width="256" /> </p> </header> </div> )}
export default App
และเนื่องจาก เราสามารถใช้ Object destructuring ได้ ก็ ปรับตรง useReducer
เป็นแบบนี้ซะ จะได้ไม่ต้องเรียก state.xxx
บ่อยๆ
const [{ cat, isFetching, count }, dispatch] = useReducer(reducer, initialState)
สุดท้ายไฟล์ src/App.js
จะได้แบบนี้
import React, { useReducer, useEffect } from 'react'import axios from 'axios'import logo from './logo.svg'import './App.css'
const fetchCat = () => axios.get('https://aws.random.cat/meow')
const initialState = { isFetching: false, cat: {}, count: 0}
const reducer = (state, { type, payload }) => { switch (type) { case 'FETCH_CAT_PENDING': return { ...state, isFetching: true } case 'FETCH_CAT_SUCCESS': return { ...state, isFetching: false, cat: payload } case 'COUNTER_CLICK': return { ...state, isFetching: false, count: payload } default: return state }}
const App = () => { const [{ cat, isFetching, count }, dispatch] = useReducer(reducer, initialState)
useEffect(() => { dispatch({ type: 'FETCH_CAT_PENDING' })
fetchCat().then((response) => { dispatch({ type: 'FETCH_CAT_SUCCESS', payload: response.data }) }) }, [])
if (isFetching) { return <p>Loading....</p> }
return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p>
<p>You click {count} times</p>
<button style={{ padding: '8px 16px', borderRadius: 4, fontSize: '1.25rem' }} onClick={() => { dispatch({ type: 'COUNTER_CLICK', payload: count + 1 }) }} > Click me </button>
<p> <img src={cat && cat.file} alt="Cat" width="256" /> </p> </header> </div> )}
export default App
ทดลองรันหน้าเว็บอีกครั้ง
สุดท้าย
นอกเหนือจากตัวอย่างที่กล่าวมา ตัว React Hooks ก็ยังมีอะไรให้ทดลองเล่นอีกเยอะครับ และเนื่องจากว่ามันก็ยังเป็นตัว Beta ฉะนั้น เมื่อตอนที่ release แล้ว syntax หรือวิธีการเขียน ก็อาจจะแตกต่างจากตอนปัจจุบันก็ได้ ฉะนั้นใครที่ลองเล่นกับมันอยู่ ก็ต้องเผื่อจุดนี้ไว้ด้วยนะครับ
ตัวอย่าง Hooks ต่างๆ ที่น่าสนใจ รวมเป็น Collection หรือ Guide เช่น
และๆ ตัว Recompose ที่หลายๆคนชอบใช้กัน เค้าเลิก maintenance แล้ว จะไม่มี feature ใหม่ๆแต่ยังใช้ได้อยู่เนาะ เพราะตัวคนที่เค้าเริ่มทำ Recompose ขึ้นมาเพื่อแก้ปัญหาต่างๆ ตัว React Hooks มันตอบโจทย์หมดแล้ว ก็เลยคิดว่า น่าจะเริ่มไปใช้ Hooks กันแน่ๆครับ ก็แน่ละ คนทำ recompose เค้าก็ทำงานอยู่ Facebook นี่เนาะ
Happy Coding ❤️
- Authors
-
Chai Phonbopit
เป็น Web Dev ในบริษัทแห่งหนึ่ง ทำงานมา 10 ปีกว่าๆ ด้วยภาษาและเทคโนโลยี เช่น JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจในเรื่องของ Blockchain และ Crypto กำลังหัดเรียนภาษา Rust