新一代 React API — React Hooks

React Conf 2018 React Today and Tomorrow 重點回顧

谷哥
谷哥
Oct 29, 2018 · 11 min read
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 '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 一直都在只是我們沒有發現而已吧!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store