React Hooks 是 React 16.7.0-alpha
版本推出的新特性,想嘗試的同窗安裝此版本便可。html
React Hooks 要解決的問題是狀態共享,是繼 render-props 和 higher-order components 以後的第三種狀態共享方案,不會產生 JSX 嵌套地獄問題。前端
狀態共享可能描述的不恰當,稱爲狀態邏輯複用會更恰當,由於只共享數據處理邏輯,不會共享數據自己。react
不久前精讀分享過的一篇 Epitath 源碼 - renderProps 新用法 就是解決 JSX 嵌套問題,有了 React Hooks 以後,這個問題就被官方正式解決了。
爲了更快理解 React Hooks 是什麼,先看筆者引用的下面一段 renderProps 代碼:git
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 解決的也是這個問題:github
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 相比,這個組件是沒有狀態的。npm
React Hooks 帶來的好處不只是 「更 FP,更新粒度更細,代碼更清晰」,還有以下三個特性:redux
第二點展開說一下: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:useFriendStatusBoolean
與 useFriendStatusString
, useFriendStatusString
是利用 useFriendStatusBoolean
生成的新 Hook,這兩個 Hook 能夠給不一樣的 UI:FriendListItem
、FriendListStatus
使用,而由於兩個 Hooks 數據是聯動的,所以兩個 UI 的狀態也是聯動的。框架
順帶一提,這個例子也能夠用來理解 對 React Hooks 的一些思考 一文的那句話:「有狀態的組件沒有渲染,有渲染的組件沒有狀態」:函數
useFriendStatusBoolean
與 useFriendStatusString
是有狀態的組件(使用 useState
),沒有渲染(返回非 UI 的值),這樣就能夠做爲 Custom Hooks 被任何 UI 組件調用。FriendListItem
與 FriendListStatus
是有渲染的組件(返回了 JSX),沒有狀態(沒有使用 useState
),這就是一個純函數 UI 組件,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。
在 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 又會成爲一套新的前端規範。
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 就像代碼級別的約定,大大下降了代碼複雜度。
由於 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 組件能夠維護狀態(也能夠不維護狀態,作個二傳手)。所以能夠考慮在 「有狀態的組件沒有渲染,有渲染的組件沒有狀態」 這句話後面加一句:沒渲染的組件也能夠沒狀態。
把 React Hooks 看成更便捷的 RenderProps 去用吧,雖然寫法看上去是內部維護了一個狀態,但其實等價於注入、Connect、HOC、或者 renderProps,那麼如此一來,使用 renderProps 的門檻會大大下降,由於 Hooks 用起來實在是太方便了,咱們能夠抽象大量 Custom Hooks,讓代碼更加 FP,同時也不會增長嵌套層級。
討論地址是: 精讀《React Hooks》 · Issue #111 · dt-fe/weekly
若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。