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

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

วันนี้จะมาพูดถึงเรื่อง 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 นี้ครับ

จริงๆแล้ว 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

  1. ต้องเรียก Hook ที่ส่วน Top Level ของ function เท่านั้น
  2. ต้องเรียก 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 component
  • useState 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-hooks
cd 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 ได้แล้ว

useState

ต่อมาลองทำ 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>
);

ทดลองดูหน้าเว็บ แต่ๆ

useEffect() loop

เว็บเรามันทำการ 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 เป็น argument
  • useReducer : 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;

ทดลองรันหน้าเว็บอีกครั้ง

Finish

สุดท้าย

นอกเหนือจากตัวอย่างที่กล่าวมา ตัว React Hooks ก็ยังมีอะไรให้ทดลองเล่นอีกเยอะครับ และเนื่องจากว่ามันก็ยังเป็นตัว Beta ฉะนั้น เมื่อตอนที่ release แล้ว syntax หรือวิธีการเขียน ก็อาจจะแตกต่างจากตอนปัจจุบันก็ได้ ฉะนั้นใครที่ลองเล่นกับมันอยู่ ก็ต้องเผื่อจุดนี้ไว้ด้วยนะครับ

ตัวอย่าง Hooks ต่างๆ ที่น่าสนใจ รวมเป็น Collection หรือ Guide เช่น

Recompose

และๆ ตัว Recompose ที่หลายๆคนชอบใช้กัน เค้าเลิก maintenance แล้ว จะไม่มี feature ใหม่ๆแต่ยังใช้ได้อยู่เนาะ เพราะตัวคนที่เค้าเริ่มทำ Recompose ขึ้นมาเพื่อแก้ปัญหาต่างๆ ตัว React Hooks มันตอบโจทย์หมดแล้ว ก็เลยคิดว่า น่าจะเริ่มไปใช้ Hooks กันแน่ๆครับ ก็แน่ละ คนทำ recompose เค้าก็ทำงานอยู่ Facebook นี่เนาะ

Happy Coding ❤️
Chai

Chai Phonbopit : Developer แห่งหนึ่ง • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football

บทความล่าสุด