/React, Hook

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

前言

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


什麼是 Hooks ?

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

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

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

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

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 的部分使用 HooksuseState 創建,需要注意 HooksFunction 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 :在 componentDOMrender 至畫面後執行。
  2. componentDidUpdate :當 componentState 改變時執行。
  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 改變時會先執行 useEffectreturn 的內容後,再執行 useEffect

State 每次改變都觸發 useEffect

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

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

Function Components 的生命週期

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

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

其實 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 ,當中包含了 useStateuseEffect

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 用來新增/移除代辦事項,時間顯示則是用 useEffectTodo 第一次 render 的時候執行 setInterval 每秒更新,且會在移除 components 時執行 returncleanInterval 來停止:

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


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

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

參考文章

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