天天看點

79.精讀《React Hooks》

1 引言

React Hooks 是 React

16.7.0-alpha

版本推出的新特性,想嘗試的同學安裝此版本即可。

React Hooks 要解決的問題是狀态共享,是繼 render-props 和 higher-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 status = useFriendStatusString(props);

  return <li>{status}</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 元件,

利用 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(() => {
    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,同時也不會增加嵌套層級。