精讀《React Hooks》

blank

精讀《React Hooks》

1 引言

React Hooks是React 16.7.0-alpha版本推出的新特性,想嘗試的同學安裝此版本即可。

React Hooks要解決的問題是狀態共享,是繼render-propshigher-order components之後的第三種狀態共享方案,不會產生JSX嵌套地獄問題。

狀態共享可能描述的不恰當,稱為狀態邏輯復用會更恰當,因為隻共享數據處理邏輯,不會共享數據本身。

不久前精讀分享過的一篇Epitath源碼- renderProps新用法就是解決JSX嵌套問題,有了React Hooks之後,這個問題就被官方正式解決了。

為了更快理解React Hooks 是什麼,先看筆者引用的下面一段renderProps 代碼:

function App() { return ( <Toggle initial={false}> {({ on, toggle }) => ( <Button type="primary" onClick={toggle}> Open Modal </Button> <Modal visible={on} onOk={toggle} onCancel={toggle} /> )} </Toggle> ) }

恰巧,React Hooks 解決的也是這個問題:

function App() { const [open, setOpen] = useState(false); return ( <> <Button type="primary" onClick={() => setOpen(true)}> Open Modal </Button> <Modal visible={open} onOk={() => setOpen(false)} onCancel={() => setOpen(false)} /> </> ); }

可以看到,React Hooks 就像一個內置的打平renderProps 庫,我們可以隨時創建一個值,與修改這個值的方法。看上去像function形式的setState,其實這等價於依賴注入,與使用setState相比,這個組件是沒有狀態的

2 概述

React Hooks 帶來的好處不僅是“更FP,更新粒度更細,代碼更清晰”,還有如下三個特性:

  1. 多個狀態不會產生嵌套,寫法還是平舖的(renderProps 可以通過compose 解決,可不但使用略為繁瑣,而且因為強制封裝一個新對象而增加了實體數量)。
  2. Hooks 可以引用其他Hooks。
  3. 更容易將組件的UI 與狀態分離。

第二點展開說一下:Hooks 可以引用其他Hooks,我們可以這麼做:

import { useState, useEffect } from "react"; // 底层Hooks, 返回布尔值:是否在线function useFriendStatusBoolean(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; } // 上层Hooks,根据在线状态返回字符串:Loading... or Online or Offline function useFriendStatusString(props) { const isOnline = useFriendStatusBoolean(props.friend.id); if (isOnline === null) { return "Loading..."; } return isOnline ? "Online" : "Offline"; } // 使用了底层Hooks 的UI function FriendListItem(props) { const isOnline = useFriendStatusBoolean(props.friend.id); return ( <li style={{ color: isOnline ? "green" : "black" }}>{props.friend.name}</li> ); } // 使用了上层Hooks 的UI function FriendListStatus(props) { const statu = useFriendStatusString(props.friend.id); return <li>{statu}</li>; }

這個例子中,有兩個Hooks: useFriendStatusBooleanuseFriendStatusString , useFriendStatusString是利用useFriendStatusBoolean生成的新Hook,這兩個Hook可以給不同的UI: FriendListItemFriendListStatus使用,而因為兩個Hooks數據是聯動的,因此兩個UI 的狀態也是聯動的。

順帶一提,這個例子也可以用來理解對React Hooks的一些思考一文的那句話: “有狀態的組件沒有渲染,有渲染的組件沒有狀態”

  • useFriendStatusBooleanuseFriendStatusString是有狀態的組件(使用useState ),沒有渲染(返回非UI的值),這樣就可以作為Custom Hooks被任何UI組件調用。
  • FriendListItemFriendListStatus是有渲染的組件(返回了JSX),沒有狀態(沒有使用useState ),這就是一個純函數UI組件,

利用useState 創建Redux

Redux 的精髓就是Reducer,而利用React Hooks 可以輕鬆創建一個Redux 機制:

// 这就是Redux function useReducer(reducer, initialState) { const [state, setState] = useState(initialState); function dispatch(action) { const nextState = reducer(state, action); setState(nextState); } return [state, dispatch]; }

這個自定義Hook 的value 部分當作redux 的state,setValue 部分當作redux 的dispatch,合起來就是一個redux。而react-redux 的connect 部分做的事情與Hook 調用一樣:

// 一个Action function useTodos() { const [todos, dispatch] = useReducer(todosReducer, []); function handleAddClick(text) { dispatch({ type: "add", text }); } return [todos, { handleAddClick }]; } // 绑定Todos 的UI function TodosUI() { const [todos, actions] = useTodos(); return ( <> {todos.map((todo, index) => ( <div>{todo.text}</div> ))} <button onClick={actions.handleAddClick}>Add Todo</button> </> ); }

useReducer已經作為一個內置Hooks了,在這裡可以查閱所有內置Hooks

不過這裡需要注意的是,每次useReducer或者自己的Custom Hooks都不會持久化數據,所以比如我們創建兩個App,App1與App2:

function App1() { const [todos, actions] = useTodos(); return <span>todo count: {todos.length}</span>; } function App2() { const [todos, actions] = useTodos(); return <span>todo count: {todos.length}</span>; } function All() { return ( <> <App1 /> <App2 /> </> ); }

這兩個實例同時渲染時,並不是共享一個todos 列表,而是分別存在兩個獨立todos 列表。也就是React Hooks 只提供狀態處理方法,不會持久化狀態。

如果要真正實現一個Redux功能,也就是全局維持一個狀態,任何組件useReducer都會訪問到同一份數據,可以和useContext一起使用。

大體思路是利用useContext共享一份數據,作為Custom Hooks的數據源。具體實現可以參考redux-react-hook

利用useEffect 代替一些生命週期

在useState 位置附近,可以使用useEffect 處理副作用:

useEffect(() => { const subscription = props.source.subscribe(); return () => { // Clean up the subscription subscription.unsubscribe(); }; });

useEffect的代碼既會在初始化時候執行,也會在後續每次rerender時執行,而返回值在析構時執行。這個更多帶來的是便利,對比一下React 版G2 調用流程:

class Component extends React.PureComponent<Props, State> { private chart: G2.Chart = null; private rootDomRef: React.ReactInstance = null; componentDidMount() { this.rootDom = ReactDOM.findDOMNode(this.rootDomRef) as HTMLDivElement; this.chart = new G2.Chart({ container: document.getElementById("chart"), forceFit: true, height: 300 }); this.freshChart(this.props); } componentWillReceiveProps(nextProps: Props) { this.freshChart(nextProps); } componentWillUnmount() { this.chart.destroy(); } freshChart(props: Props) { // do something this.chart.render(); } render() { return <div ref={ref => (this.rootDomRef = ref)} />; } }

用React Hooks 可以這麼做:

function App() { const ref = React.useRef(null); let chart: G2.Chart = null; React.useEffect(() => { if (!chart) { chart = new G2.Chart({ container: ReactDOM.findDOMNode(ref.current) as HTMLDivElement, width: 500, height: 500 }); } // do something chart.render(); return () => chart.destroy(); }); return <div ref={ref} />; }

可以看到將細碎的代碼片段結合成了一個完整的代碼塊,更維護。

現在介紹了useState useContext useEffect useRef等常用hooks,更多可以查閱: 內置Hooks ,相信不久的未來,這些API又會成為一套新的前端規範。

3 精讀

Hooks 帶來的約定

Hook 函數必須以"use" 命名開頭,因為這樣才方便eslint 做檢查,防止用condition 判斷包裹useHook 語句。

為什麼不能用condition包裹useHook語句,詳情可以見官方文檔,這裡簡單介紹一下。

React Hooks並不是通過Proxy或者getters實現的(具體可以看這篇文章React hooks: not magic, just arrays ),而是通過數組實現的,每次useState都會改變下標,如果useState被包裹在condition中,那每次執行的下標就可能對不上,導致useState導出的setter更新錯數據。

雖然有eslint-plugin-react-hooks插件保駕護航,但這第一次將“約定優先”理念引入了React框架中,帶來了前所未有的代碼命名和順序限制(函數命名遭到官方限制,JS自由主義者也許會暴跳如雷),但帶來的便利也是前所未有的(沒有比React Hooks 更好的狀態共享方案了,約定帶來提效,自由的代價就是回到renderProps or HOC,各團隊可以自行評估) 。

筆者認為,React Hooks 的誕生,也許來自於這個靈感:“不如通過增加一些約定,徹底解決狀態共享問題吧!”

React約定大於配置腳手架nextjs umi以及筆者的pri都通過有“約定路由”的功能,大大降低了路由配置複雜度,那麼React Hooks就像代碼級別的約定,大大降低了代碼複雜度。

狀態與UI 的界限會越來越清晰

因為React Hooks的特性,如果一個Hook不產生UI,那麼它可以永遠被其他Hook封裝,雖然允許有副作用,但是被包裹在useEffect裡,總體來說還是挺函數式的。而Hooks 要集中在UI 函數頂部寫,也很容易養成書寫無狀態UI 組件的好習慣,踐行“狀態與UI 分開” 這個理念會更容易。

不過這個理念稍微有點蹩腳的地方,那就是“狀態” 到底是什麼。

function App() { const [count, setCount] = useCount(); return <span>{count}</span>; }

我們知道useCount算是無狀態的,因為React Hooks本質就是renderProps或者HOC的另一種寫法,換成renderProps就好理解了:

<Count>{(count, setCount) => <App count={count} setCount={setCount} />}</Count>; function App(props) { return <span>{props.count}</span>; }

可以看到App 組件是無狀態的,輸出完全由輸入(Props)決定。

那麼有狀態無UI的組件就是useCount了:

function useCount() { const [count, setCount] = useState(0); return [count, setCount]; }

有狀態的地方應該指useState(0)這句,不過這句和無狀態UI組件App的useCount()很像,既然React把useCount成為自定義Hook,那麼useState就是官方Hook,具有一樣的定義,因此可以認為useCount是無狀態的, useState也是一層renderProps,最終的狀態其實是useState這個React內置的組件。

我們看renderProps 嵌套的表達:

<UseState> {(count, setCount) => ( <UseCount> {" "} {/**虽然是透传,但给count 做了去重,不可谓没有作用*/} {(count, setCount) => <App count={count} setCount={setCount} />} </UseCount> )} </UseState>

能確定的是,App 一定有UI,而上面兩層父級組件一定沒有UI。為了最佳實踐,我們盡量避免App 自己維護狀態,而其父級的RenderProps 組件可以維護狀態(也可以不維護狀態,做個二傳手)。因此可以考慮在“有狀態的組件沒有渲染,有渲染的組件沒有狀態” 這句話後面加一句:沒渲染的組件也可以沒狀態。

4 總結

把React Hooks 當作更便捷的RenderProps 去用吧,雖然寫法看上去是內部維護了一個狀態,但其實等價於注入、Connect、HOC、或者renderProps,那麼如此一來,使用renderProps 的門檻會大大降低,因為Hooks 用起來實在是太方便了,我們可以抽像大量Custom Hooks,讓代碼更加FP,同時也不會增加嵌套層級。

5 更多討論

討論地址是: 精讀《React Hooks》 · Issue #111 · dt-fe/weekly

如果你想參與討論,請點擊這裡,每週都有新的主題,週末或週一發布。前端精讀-幫你篩選靠譜的內容。

What do you think?

Written by marketer

blank

編寫自己的SVG 圖標庫

blank

在深談TCP/IP三步握手&四步揮手原理及衍生問題—長文解剖IP——節選版