新一代 React API — React Hooks

React Conf 2018 React Today and Tomorrow 重點回顧

React 的 Google 搜尋量終於在 2018 打敗 jQuery

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)
Wrapper hell
  • Giant Components: 以下圖為例,透過 life cycle API 處理各種 side effects,當要處理的 side effects 變得越來越多,component 也會變得越來越龐大
a little bit giant 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>
)
}
  1. render function 拿掉,改成直接 return
  2. functional component 沒有 constructor,所以也拿掉,包含裡面 state initialization 和 event binding 的邏輯
  3. input 的 value 本來是 this.state.name,但 functional component 沒有 state,所以 name 從哪來?先宣告一個 name 的變數然後暫時給個問號
  4. handleNameChange 裡面本來呼叫 this.setState(),但 function component 也沒有 setState,只好先宣告一個 setName 的變數然後也暫時給個問號

所以 name 和 setName 從哪裡來?React hooks 提供了 useState 來取代 class component 的 state 和 setState,結果如下:

import { useState } from 'react
export 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 'react
export 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 'react
export 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.Provider>
{theme => (
<div style={theme} />
}
</ThemeContext.Provider>
)
}

改寫之後:

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 一直都在只是我們沒有發現而已吧!