hook!

翡翡發表於2018-11-12

在react conf 2018上,react釋出了一個新的提案hook。穩定的正式版可能要等一兩個月之後才能出來,目前可以在v16.7.0-alpha上試用到rfc上各種提問。

那麼這個hook到底是個什麼呢,官方的定義是這樣的

Hooks are a new feature proposal that lets you use state and other React features without writing a class.

這是一個比class更直觀的新寫法,在這個寫法中react元件都是純函式,沒有生命週期函式,但可以像class一樣擁有state,可以由effect觸發生命週期更新,提供一種新的思路來寫react。(雖然官方再三宣告我們絕對沒有要拿掉class的意思,但hook未來的目標是覆蓋所有class的應用場景)

其實在看demo演示的時候我是十分抗拒的,沒有生命週期函式的react是個什麼黑魔法,雖然程式碼變得乾淨了不少,但寫法實在是發生了很大的轉變,有種脫離掌控的不安全感,我甚至有點懷疑我能不能好好debug。

演示的最後dan的結束語是這樣的

hook代表了我們對react未來的願景,也是我們用來推動react前進的方法。因此我們不會做大幅的重寫,我們會讓舊的class模式和新的hook模式共存,所以我們可以一起慢慢的接納這個新的react。

我接觸react已經四年了,第一次接觸它的時候,我第一個想問的是,為什麼要用jsx。第二個想問的是,為什麼要用這個logo,畢竟我們又不是叫atom,也不是什麼物理引擎。現在我想到了了一個解釋,原子的型別和屬性決定了事物的外觀和表現,react也是一樣的,你可以把介面劃分為一個個獨立的元件,這些元件(component)的型別(type)和屬性(props)決定了最終介面的外觀和表現。諷刺的是,原子一直被認為是不可分的,所以當科學家第一次發現原子的時候認為這就是最小的單元,直到後來在原子中發現了電子,實際上電子的運動更能決定原子能做什麼。hook也是一樣的,我不認為hook是一個新的react特性,相反的,我認為hook能讓我更直觀的瞭解react的基本特性像是state、context、生命週期。hook能更直觀的代表react,它解釋了元件內部是如何工作的,我認為它被遺落了四年,當你看到react的logo,可以看到電子一直環繞在那裡,hook也是,它一直在這裡。

於是我決定乾了這杯安利。

試了幾個比較基本的api寫了幾個demo,程式碼在 github.com/lllbahol/re…, 完全的api還請參考官方文件 reactjs.org/docs/hooks-…

api

基本的hook有三個

  • useState(相當於state)
  • useEffect(相當於componentDidUpdate, componentDidMount, componentWillUnmount)
  • useContext(相當於Context api)

useState

const [state, setState] = useState(initialState);

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
複製程式碼

在這裡react元件就是一個簡單的function

  • useState(initialState)也是一個函式,定義了一個state,初始值為initialState,返回值是一個陣列,0為state的值,1為setState的方法。

  • 當state發生變化時,函式元件重新整理。

  • 可以useState多次來定義多個state,react會根據呼叫順序來判斷。

你一定也寫過一個龐大的class, 有一堆handler函式,因為要setState所以不能挪到元件外面去,然後render函式就被擠出了頁面,每次想看render都要把頁面滾到底下。

現在因為useState是函式,所以它可以被挪到元件外面,連帶handler一起,下面是一個具體一點的表單例子。

import React, { useState } from 'react';

// 表單元件,有name, phone兩個輸入框。
export default () => {
  const name = useSetValue('hello');
  const phone = useSetValue('120');
  return (
    <React.Fragment>
      <Item {...name} />
      <br />
      <Item {...phone} />
    </React.Fragment>
  );
}

// controlled input component
const Item = ({ value, setValue }) => (
  <React.Fragment>
    <label>{value}</label>
    <br />
    <input value={value} onChange={setValue} />
  </React.Fragment>
);

// 可以將state連同handler function一起挪到元件外面。
// 甚至可以export出去,讓其他元件也能使用這個state邏輯
const useSetValue = (initvalue) => {
  const [value, setValue] = useState(initvalue);
  const handleChange = (e) => {
    setValue(e.target.value);
  }
  return {
    value,
    setValue: handleChange,
  };
}

複製程式碼

useEffect

這個api可以讓你在函式元件中使用副作用(use side effects),常見的會產生副作用的方式有獲取資料,更新dom,繫結事件監聽等,render只負責渲染,一般會等到dom載入好之後再去呼叫這些副作用方法。

useEffect(didUpdate/didMount);

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);
複製程式碼

useEffect可以接受兩個引數

  • 第一個引數為一個effect函式,effect函式在每次元件render之後被呼叫,相當於componentDidUpdate和componentDidMount兩個生命週期之和。effect函式可以返回一個clear effect函式,會在下一次的effect函式執行之前執行,原來componentWillUnmount裡執行的東西都可以交給它。呼叫順序是:render(dom載入完成) => prevClearUseEffect => useEffect

  • 第二個引數是一個陣列,只有當陣列傳入的值發生變化時,effect才會執行。

上面的寫法如果用class實現的話應該是下面這樣的。我們按時間先後將一個會產生副作用的函式的第1次呼叫、第2-n次呼叫、解除安裝分成3截,實際上它們總是一一對應出現的,應該是一個整體。

componentDidMount() {
  this.subscription = props.source.subscribe();
}

componentDidUpdate() {
  this.subscription = props.source.subscribe();
}

componentWillUnmount () {
  subscription.unsubscribe();
}
複製程式碼

具體案例可以看一個輪播元件的demo

import React, { useState, useEffect } from 'react';
import './index.css';

const IMG_NUM = 3;

export default () => {
  const [index, setIndex] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  useEffect(() => {
    // 每次元件重新整理時觸發effect, 相當cDM cDU
    if (isPlaying) {
      const timeout = setTimeout(() => {
        // 改變state, 重新整理元件
        handleNext();
      }, 2000);
      // 返回清除effect的回撥函式, 在每次effect呼叫完之後,如果有則執行
      return () => clearTimeout(timeout);
    }
    // 如果不想每次render之後都調一次effect, 可以使用第二個引數作為篩選條件
  }, [index, isPlaying]);

  const handleNext = () => {
    setIndex((index + 1) % IMG_NUM);
  }
  const handlePrev = () => {
    setIndex((index - 1 + IMG_NUM) % IMG_NUM);
  }
  const handlePause = () => {
    setIsPlaying(!isPlaying);
  };
  return (
    <div>
      <div className="img">{index}</div>
      <button onClick={handlePrev}>prev</button>
      <button onClick={handlePause}>pause</button>
      <button onClick={handleNext}>next</button>
    </div>
  )
}
複製程式碼

useContext

const context = useContext(Context);

如果對react比較熟悉的話,應該用過Context這個api,用於在元件之間傳遞資料。useContext接受一個context物件(React.createContext生成),返回context.Consumer中獲得的值。

export const Context = React.createContext(null);

function Parent() {
  const someValue = 'haha';
  return (
    <Context.Provider value={someValue}>
      <DeepTree>
       	<DeepChild />
      </DeepTree>
    </Context.Provider>
  );
}
複製程式碼
function DeepChild() {
  const someValue = useContext(Context);
  return (<div>{someValue}</div>)
}

複製程式碼

16.7之前的Consumer寫法是render props

function DeepChild() {
  return (
    <Context.Consumer>
      {
        (someValue) => <div>{someValue}</div>
      }
    </Context.Consumer>
  )
}
複製程式碼

似乎還能忍受,但是但是,為了避免不必要的重新整理一般推薦用多個Context來傳遞重新整理週期不同的資料,因此按原來的render-props寫法很容易陷入多重巢狀地獄(wrapper-hell),很有可能你真正的渲染程式碼在十幾個縮排後面才開始出現。繼程式碼上下滾問題之後我們又出現了程式碼左右滾問題。

<Consumer1>
  {
    (value1) => (
      <Consumer2>
        {
          (value2) => (
            ...
          )
        }
      </Consumer2>
    )
  }
</Consumer1>

// 我怎麼還沒有被同事打死?

複製程式碼

useReducer

還有一堆高階hook

其中有一個useReducer

就是大家熟悉的那個redux裡的reducer,來段模板程式碼讓大家回憶一下。

const mapStateToProps = createStructuredSelector({
	...
});

const mapDispatchToProps = (dispatch) => ({
  ...
});

const withReducer = injectReducer({ ... });

const withConnect = connect(mapStateToProps, mapDispatchToProps);

export default compose(withReducer, withConnect)(Component);
複製程式碼

以上的這些,使用了useReducer之後都沒有了。

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, {count: initialCount});
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'reset'})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}
複製程式碼

我還用useReducer實現了一個todo的demo,程式碼分了好幾個檔案就不放上來了 github.com/lllbahol/re…

為什麼要用hook

除了上面提到的,還有官方羅列出來的一些時常會在寫class時遇到的麻煩

  • class元件間不能複用與state關聯的程式碼,hook可以做到這一點。
  • 複雜而龐大的class元件很難被理解,hook能夠讓你把元件拆成更小的獨立單元
  • 理解class是一件困難的事,無論是對人還是對開發工具而言都是這樣。比如class裡面的this指向的是元件,在箭頭函式寫法出來之前,我們不得不手動繫結this到呼叫函式的物件上。

總的來說

用react也好久了,工程越寫越複雜,元件間的資料傳遞是一個很大的問題,從傳統的傳回撥函式,到跨多層多元件共享資料的時候使用redux,後來嫌模板程式碼太多又自己封了一層render-props結果掉進wrapper巢狀地獄的坑裡,Context出來的時候開心了一會兒然後發現依然在坑裡。寫是能寫的,就是恐懼,每寫一層,我的程式碼就又縮排了三個tab,離被同事打死又前進三步。

useContext,useReducer的用法讓我想到了高階,不同的是可以直接用變數接住而不是掛在props上,因此不用考慮props名衝突問題,但能達到高階一層層包裹資料的效果。

從現有的文件來看,新的api非常的多,一些是我們熟悉的用法一些則是完全新的東西,且暫時還沒能覆蓋所有生命週期場景(比如getDeriveStateFromProps),但不著急,可以一步一步來。

hook正式版釋出之後我還會來更新一次這個文件,在工程里正式使用一段時間之後會再更新一次,先奶一口。

參考

  1. www.youtube.com/watch?v=dpw… 官方介紹hook的視訊
  2. reactjs.org/docs/hooks-… 官方文件
  3. reactjs.org/docs/hooks-… 一些常見問題的官方解答

相關文章