React | 為了與 Hooks 相遇 - Function Components 升級記
前言
前幾天 React
釋出 16.8 版本的消息在各群組上傳得沸沸揚揚,理由是因為這一次的改版新增了 Hooks
,讓 Function Components
變得和以往不同!
什麼是 Hooks ?
在提到 Hooks
前,必須先理解 React
在 16.8 版本前撰寫 Component
的兩種方式:
Function Components
:使用一般的function
來宣告,作為函式他接收props
做參數,並回傳一個React
的DOM
元素。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
中,有以下幾種方式可以在各個週期的時候執行額外的事件:
componentDidMount
:在component
將DOM
給render
至畫面後執行。componentDidUpdate
:當component
的State
改變時執行。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
:
接著當 State
改變時會先執行 useEffect
中 return
的內容後,再執行 useEffect
:
由上方的 gif 可以發現,當每一次改變 State
時都會執行兩次生命週期內的事件,先是執行 return
,再執行 useEffect
內的。
經過實測,可以把 useEffect
分成三個部分:
- 橘色框框的內容為
componentDidMount
,會在render
後執行。 - 綠色框框的內容為
componentWillUnmount
,會在components
移除時執行。 - 紅色框框的內容為
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
來停止:
本文介紹了在 React
新版本中增加的 Hooks
其中兩個功能,在閱讀官方文件的時候還有許多滿有趣的 Hooks
,包括相似 Redux
的 useReducer
和自定義的 Hooks
,今後會再持續分享一些學習的過程。
如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!
參考文章