新一代 React API — React Hooks
React Conf 2018 React Today and Tomorrow 重點回顧
Sophie Alpert ( ReactJS team manager ) 提到團隊在過去一年致力於開發:
- Time Slicing
- Suspense
- Profiler
目前已經在 v16.5 開始支援 DevTools profiler plugin,v16.6 提供使用 suspense API,更多細節可以參考 Dan Abramov 在 JSConf Iceland 2018 的演講影片
What in React still sucks?
Problems Today
Sophie Alpert 再提到目前使用 React 開發遇到的三大問題:Reusing Logic、Giant Components、Confusing Classes
- Reusing Logic: 為了避免程式碼出現重複的邏輯,我們透過 HOC 和 Render Props 來解決這個問題,同時卻帶來許多問題,例如需要花額外的時間重構或是產生很深的巢狀構造(也就是所謂的 Wrapper Hell)
- Giant Components: 以下圖為例,透過 life cycle API 處理各種 side effects,當要處理的 side effects 變得越來越多,component 也會變得越來越龐大
- Confusing Classes: 為了使用 life cycle API 或是 state,必須理解 ES6 class的用法,對於初學者來說會有一段學習曲線,另外 ReactJS 開發團隊也發現 ES6 class 會影響 compile 時期的效能調校
Dan Abramov 將以上三個問題歸納出問題的核心
React doesn’t provide a stateful primitive simpler than a class component.
(其實早期的 Mixins 就可以解決這個問題,但 Mixins 帶來的優點不如伴隨而來的缺點,可以參考這篇 Mixins Considered Harmful)
ReactJS 團隊該如何解決這個問題呢?
Solutions Tomorrow
ReactJS 團隊提出了 hooks 這個概念:
Solutions for state & setState — useState
如果我們要做出一個像上面一樣有個 input 的 component,大概會這樣寫:
export default class Greeting extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'Mary'
}
this.handleNameChange = this.handleNameChange.bind(this)
} handleNameChange(e) {
this.setState({
name: e.target.value
})
} render() {
return (
<div>
<label>Name</label>
<input
value={this.state.name}
onChange={this.handleNameChange}
/>
</div>
)
}
}
那如果我們不用 class component 而改用 functional component 呢?
export default function Greeting() {
const name = ???
const setName = ???
function handleNameChange(e) {
setName(e.target.value)
} return (
<div>
<label>Name</label>
<input
value={name}
onChange={handleNameChange}
/>
</div>
)
}
- render function 拿掉,改成直接 return
- functional component 沒有 constructor,所以也拿掉,包含裡面 state initialization 和 event binding 的邏輯
- input 的 value 本來是 this.state.name,但 functional component 沒有 state,所以 name 從哪來?先宣告一個 name 的變數然後暫時給個問號
- handleNameChange 裡面本來呼叫 this.setState(),但 function component 也沒有 setState,只好先宣告一個 setName 的變數然後也暫時給個問號
所以 name 和 setName 從哪裡來?React hooks 提供了 useState 來取代 class component 的 state 和 setState,結果如下:
import { useState } from 'reactexport default function Greeting() {
const [name, setName] = useState(‘Mary’) // 傳入 default value
function handleNameChange(e) {
setName(e.target.value)
} return (
<div>
<label>Name</label>
<input
value={name}
onChange={handleNameChange}
/>
</div>
)
}
是不是很神奇呢?
想必有經驗的開發者一定會想到:那如何改寫 life cycle API?
Solutions for life cycle APIs — useEffect
如果我們要偵測瀏覽器寬度,大概會這樣寫:
export default class Greeting extends React.Component {
constructor(props) {
super(props)
this.state = {
width: window.innerWidth
}
this.handleResize = this.handleResize.bind(this)
} handleResize(e) {
this.setState({
width: window.innerWidth
})
} componentDidMount() {
window.addEventListener('resize', this.handleResize)
} componentWillUnmount() {
window.removeEventListener('resize', this.handleResize)
} render() {
return (
<div>
{this.state.width}
</div>
)
}
}
如果改用 functional component 但仍然可以處理 side effect 呢?基於 useState 會改寫成:
import { useState, useEffect } from 'reactexport default function Greeting() {
const [width, setWidth] = useState(window.innerWidth) // 傳入 componentDidMount 時呼叫的 function
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener(‘resize’, handleResize)
// 回傳 componentWillUnMount 時呼叫的 function
return () => {
window.removeEventListener(‘resize’, handleResize)
}
}) return (
<div>
{width}
</div>
)
}
甚至我們可以進一步將和 resize 有關的邏輯拆分開來寫成 custom hook,提供更好的共用性以及可測試性:
import { useState, useEffect } from 'reactexport default function Greeting() {
const width = useWindowWidth() return (
<div>
{width}
</div>
)
}// custom hook
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth) useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}) return width
}
Solutions for context — useContext
原先基於 Render Props 的 Context API,也可以使用 React hook 改寫,原本的寫法:
import { ThemeContext } from './context' export default function Greeting() {
return (
<ThemeContext.Consumer>
{theme => (
<div style={theme} />
}
</ThemeContext.Consumer>
)
}
改寫之後:
import { useContext } from 'react'
import { ThemeContext } from './context'export default function Greeting() {
const theme = useContext(ThemeContext)
return (
<div style={theme} />
)
}
是不是簡潔許多了呢?雖然在 v16.7 alpha 版本可以開始試用,但千萬別急著用 hooks 重寫所有的 components,畢竟目前還在 proposal 階段。也可以在開發新 component 時試試看,再慢慢習慣 hooks 的概念
後記
Dan Abaramov 最後提到他個人對 hooks 的想法,我覺滿有趣的。
(以下這段話,盡可能表達他的原意,但翻譯上還是會有落差,敬請見諒)
我們知道所有物質都是由原子組成的,當科學家發現原子時,取名叫做 atom(意指不可分割),原子決定了物質的行為和外貌,但後來又在原子裡發現電子的存在,電子的特性甚至更能解釋原子之間的交互作用。而 hooks 之於 components 就好比電子之於原子, hooks 並不是新的東西,但它更能表現 components 之間的運作關係,React 的 logo 看起來就像是電子繞著原子,其實 hooks 一直都在只是我們沒有發現而已吧!