/React, Hook

React | 為了與 Hooks 相遇 - Function Components 升級記

前言

前幾天 React 釋出 16.8 版本的消息在各群組上傳得沸沸揚揚,理由是因為這一次的改版新增了 Hooks ,讓 Function Components 變得和以往不同!


什麼是 Hooks ?

在提到 Hooks 前,必須先理解 React 在 16.8 版本前撰寫 Component 的兩種方式:

  1. Function Components :使用一般的 function 來宣告,作為函式他接收 props 做參數,並回傳一個 React 的 DOM 元素。
  2. Class Components :使用 ES6 的 Class 語法糖創建一個 React 下的子類別,也可藉由 React 內的 render 函式回傳 DOM 元素。

基本上兩種方式創建出來的 DOM 沒有不同,差別在於 Class Components 擁有自身的 State (狀態)及 Lifecycle (生命週期)。

但記得嗎?就像我上方提到的,這是 React 在 16.8 版本前的事情了, Hooks 的出現,改變了 Function Components ,讓他擁有專屬的 useState 和 useEffect 來管理狀態及的生命週期。

Hooks 使用

安裝/更新最新版本

首先將原有或新專案的 React 版本升級為 16.8 版本,可透過以下指令安裝最新版本:

npm install react

或者將原有版本升級:

npm update react

升級後便可以在 react 中使用 Hooks 的新功能:

import React, { useState, useEffect } from 'react'

管理 State

就如上所說,新版加入的 Hooks 可以為 Function Components 申裝 State 的功能,這個其中一個核心全都歸咎於 useState 。

在以往的 Class Components 中管理 State ,需在 constructor 中做初始設置,之後便可使用 this.setState 更新 State 的內容,如下:

import React from 'react'

class Todo extends React.Component {
    constructor(props){
        super(props)
        
        //設置 state
        this.state = {
            listName: 'default value'
        }
        this.changeListName = this.changeListName.bind(this)
    }

    changeListName(e){
        //改變 state
        this.setState({listName: e.target.value},
            ()=>{console.log(this.state)})
    }

    render(){
        return(
            <input value={this.state.listName} onChange={this.changeListName} />
        )
    }
}

使用了 Hooks 後的 Function Components 則以 useState 進行管理, Hooks 版本:

import React, { useState } from 'react'

const Todo = props => {
    //設置 state
    const [listName, setListName] = useState('default value')

    //改變 state
    const changeListName = e =>{
        setListName(e.target.value) 
        console.log(listName)
    }

    return (
        <input value={listName} onChange={changeListName} />
    )
}

兩種版本呈現的結果都一樣,差別在於設置及修改 State 的部分使用 Hooks 的 useState 創建,需要注意 Hooks 是 Function Components 專屬的功能,在 Class Components 中無法使用 Hooks 。

const [listName, setListName] = useState('default value')

useState 會回傳一個陣列,陣列中分別是「保管 State 值的變數( listName )」和「更新 State 的函式( setListName )」,因此在接收回傳的值時, React 選擇使用了 ES6 解構賦值的方式處理。

如果使用一個變數去接收的話,則會變成:

const listName = useState('default')listName[0] //listName 的值listName[1] //上方的 setListName 函式

Hooks 可以多次使用,也可各種型態設置 State ,包含「物件」、「陣列」等等:

//string
const [listName, setListName] = useState('default value')

//object
const [list, setList] = 
      useState({ key: 1, name: '預設事項' })

//array
const [todoList, setTodoList] = 
      useState([{ key: 1, name: '預設事項' },
                { key: 2, name: '預設事項2' }])

使用 State 時,不需以 this.state.listName 來取值,在 Function Components 中直接以 listName 就可以取到 State 值了。

更新時也不必像 this.setState({listName:value}) 使用物件做更新,因為每個 State 都由一個 useState 創建,因此只需要直接把要更新的值給對應的更新函式處理,例如 setListName(value) 。

了解 Lifecycle

既然有了 State ,那當 State 改變時能不能同時執行些事件,當然! Hooks 另一個核心功能 useEffect ,在 Function Components 中,可以利用它創建生命週期。

在 Class Components 中,有以下幾種方式可以在各個週期的時候執行額外的事件:

  1. componentDidMount :在 component 將 DOM 給 render 至畫面後執行。
  2. componentDidUpdate :當 component 的 State 改變時執行。
  3. componentWillUnmount :當 component 被移除時執行。

但是 Function Components 裡只需要一個 useEffect ,就可以將三個願望一次滿足:

const Todo = props => {
    //設置 state
    const [listName, setListName] = useState('default value')

    //改變 state
    const changeListName = e =>{
        setListName(e.target.value) 
    }

    //設置 lifecycle
    useEffect(()=>{
        //componentDidMount 及 componentDidUpdate
        console.log(`更新後的 State ${listName}`)

        //componentDidUpdate 及 componentWillUnmount
        return(()=>{
            console.log(`更新前的 State ${listName}`)
        })
    })

    return (
        <input value={listName} onChange={changeListName} />
    )
}

這裡可能會有許多人對 useEffect 感到困惑,它到底是怎麼運作的?可以先看上方的執行結果後再接著解釋:

在 Function Components 執行 render 後便會先執行一次 useEffect :

首次 render 時執行的生命週期

接著當 State 改變時會先執行 useEffect 中 return 的內容後,再執行 useEffect :

State 每次改變都觸發 useEffect

由上方的 gif 可以發現,當每一次改變 State 時都會執行兩次生命週期內的事件,先是執行 return ,再執行 useEffect 內的。

經過實測,可以把 useEffect 分成三個部分:

Function Components 的生命週期

  1. 橘色框框的內容為 componentDidMount ,會在 render 後執行。
  2. 綠色框框的內容為 componentWillUnmount ,會在 components 移除時執行。
  3. 紅色框框的內容為 componentDidUpdate ,會在 State 改變時執行,執行順序為綠色框框到橘色框框。

但是如果 Component 中只需在 render 後執行一次獲取資料的動作,該怎麼像 Class Components 的 componentDidMount 一樣呢?

其實 useEffect 可以有第二個參數,只需要在 useEffect 的第二個參數中設置一個空陣列 [] ,且不需要加入 return 的函式,便可以讓 useEffect 只在第一次的 render 後執行,就如同 componentDidMount :

useEffect(()=>{
console.log(`只執行第一次`)
},[])

useEffect 的第二個參數不只是能傳空陣列,在陣列中可以設置一個以上的 State 名稱,讓生命週期只觸發在特定的 State 改變時:

useEffect(()=>{
console.log(`只執行第一次和 listName 改變的時候`)
},[listName])

也就是說,當 listName 的值為 ‘Default value’ ,而下一個值也是 ‘Default value’ 時,那 useEffect 就不會被觸發,因為修改前後的值都相同,除非 listName 被更改為 ‘Default value’ 外的值。

最後,筆者使用 Hooks 創建一個簡單的 todolist ,當中包含了 useState 及 useEffect :

import React, { useState, useEffect } from 'react'

const Todo = props => {
    const [listName, setListName] = useState('')
    const [todoList, setTodoList] = useState([{ key: 1, name: '預設事項' }])
    const [time, setTime] = useState(new Date())

    const addTodo = () => {
        const newKey = todoList.length === 0 ? 1 : todoList[todoList.length - 1].key + 1
        setTodoList([...todoList, { key: newKey, name: listName }])
        setListName('')
    }

    const removeTodo = (listKey) => {
        let foundIndex = todoList.findIndex((list) => {
            return list.key === listKey
        })
        todoList.splice(foundIndex, 1)
        setTodoList([...todoList])
    }

    useEffect(() => {
        const timer = setInterval(tickTime, 1000)

        return (() => {
            clearInterval(timer)
        })
    }, [])

    const tickTime = () => {
        setTime(new Date())
    }

    return (
        <div>
            <p>
                {time.toString()}
            </p>
            <input value={listName} onChange={e => setListName(e.target.value)} />
            <input type='button' value='新增' onClick={addTodo} />
            {todoList.map((list) => {
                return (
                    <p key={list.key}>
                        <input type='button' value='移除' onClick={() => { removeTodo(list.key) }} />
                        {list.name}
                    </p>
                )
            })}
        </div>
    )
}

export { Todo }

上方以 useState 設置 listName 管理待辦事項的名稱、 todoList 為事項內容、 setTodoList 用來新增/移除代辦事項,時間顯示則是用 useEffect 在 Todo 第一次 render 的時候執行 setInterval 每秒更新,且會在移除 components 時執行 return 的 cleanInterval 來停止:

useState 增加/移除事項,useEffect 為顯示時間


本文介紹了在 React 新版本中增加的 Hooks 其中兩個功能,在閱讀官方文件的時候還有許多滿有趣的 Hooks ,包括相似 Redux 的 useReducer 和自定義的 Hooks ,今後會再持續分享一些學習的過程。

如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!

參考文章

  1. https://reactjs.org/docs/hooks-state.html
  2. https://reactjs.org/docs/hooks-effect.html