Try   HackMD

我也要成為 React 大師

如同龍佬!

環境建置

請參考新主流環境建置筆記。

React 基礎和 Class component

React 是 JS UI 函式庫,旨在建構單頁、行動裝置軟體。

VirtualDOM

html 元素構成一樹,VirtualDOM 會批次透過比較當前 DOM 和實際渲染內容的差異,蒐集需要重新渲染的資料並更新之,增加動態渲染元素之效率。

更新方法

由根走訪 VirtualDOM 樹並傳遞差異,決定哪些枝條要更新。

React Component

React 本於所有 HTML 元素都以 JSX 語法表達並生成,所有元素都應繼承 React.Component 物件,該物件有兩重要成員:

今後「元素」表示繼承 React.Component 物件之物件。

  • props: 不可變成員,該成員下又有多個不可變成員。

    其為元素建構子的參數,於 VirtualDOM 的生成中由親代節點傳遞至子節點,並由子節點持有參考。

    props 不可變是對「持有的節點」而言,也就是上述的子節點;對親代節點可變

    至於原因,是因為 props 是親代與子節點的溝通渠道,也就是親代節點對子節點的依賴注入,可用於實作「親代節點發生變化後傳遞至子節點比較差異、重新渲染」的邏輯。若子節點直接更改 props,親代節點並不會收到重新渲染的通知,引起不可預期的錯誤。

    詳細可參見本提問串

  • state: 可變成員。紀錄元素的狀態,其之下可以有多個可變成員。與一般成員不同的是,作為 React 稽查的對象,會觸發 Lifecycle 中 Updating 過程。

    state 雖說可變,但應透過 React.Component::setState 方法改變,以觸發生命週期相關函式,而非直接 assign,直接 assign 只發生在建構子。

至於元素的方法?將與 Lifecycle 一併介紹。

Lifecycle

所有元素皆有 Lifecycle,即可以用相同的有限狀態機描述。分為三階段,並以物件 React.Component 的形式包裝,所有元素都應繼承之。

  • Mounting: 創建元素並插入 DOM
  • Updating: 重新渲染元素
  • Unmounting: 從 DOM 裡移除元素

上圖粗體表重要方法,以下分別介紹。

render

本方法繪製「一個」HTML 元素。如果想一次繪製很多個元素,需將其包為一個,至少輸出時只能有一個。既然決定元素的外觀,那必然要實作

回傳值為以下之一。

  1. JSX element
  2. Array & fragments
  3. Protals
  4. string, numbers, booleans, null

渲染除了建構時,通常發生在 propsstate 改變時,透過分析這兩者的重新渲染元素。當某節點要重新渲染時,該節點以降所有子元素都應該重新渲染。

constructor

建構子,初始化物件成員,可不親自實作。但若要實作必須呼叫 super 建構子

componentDidMount

建立 DOM node,將渲染好的元素加入 DOM tree,可在此時建立網路 request。

componentDidUpdate

可進行 propsstate 改變的前後檢查,參數主要有二: prevPropsprevState,為走過 update 流程前的成員值。

componentDidUnmount

可拿來取消一些請求,比如元件準備離開 DOM tree 時若正在請求資料,可以取消請求。

主流 React 風格: Functional Component

流派的遞嬗

不管元素的複雜與否,渲染元素都需要使用上述較為繁雜的物件語法。注意到物件導向的一大特色就是程式碼比較長 XD,這降低小元件的撰寫速度與可讀性,故新 React 引入本風格,將物件導向必要的成分拆解為數個函式,增加開發效率。

注意「函式」本身似乎是 stateless 的,不過得益於 Javascript 中函式為「頭等函式」(First class function) 的特性,依然可以給函式夾帶變數與函式,稱為 Hooking。

直接渲染函式

對應 render 函式,不過有差異。

如果說 Class component 風格是「齊全的工具箱」,那 Functional component 便是「易於擴充的工具箱」。前者使用時我們得全面考慮物件的生命週期,並實作相對應的方法們。後者可以由簡入繁,從基本的顯示開始,需要什麼功能再透過 Hooking 擴充元素的功能。

直接渲染用的便是如此,參考官方文件,以下可以快速生成一 div 元素。

// Declaring type of props - see "Typing Component Props" for more examples
type HappyProps = {
  message: string
} /* use `interface` if exporting so that consumers can extend */

// Easiest way to declare a Function Component; return type is inferred.
const HappyElement = ({ message }: HappyProps) => <div>{message}</div>

useState: 從 stateless 轉為 stateful

對應 this.state 成員和 setState 方法。

上圖說明 Functinoal Component 視角下的生命週期。當中 Updating 有 useState 方法,其為直接渲染函式進行擴充,相當於為其賦予 state 成員與 useXXX 方法。

使用上可利用 const 區域變數擷取 useState 的兩個回傳值,前者為 stateful 變數,後者為前者的設定用函式。

先以 Class component 舉例,假設有以下 Class component 實作按鈕累加計數:

// first defind type of prop
type CompWithStateProps = {
  start_count: number
}

// then define class component
class ClassWithState extends React.Component {
  state: { count: number }
  constructor(props: CompWithStateProps) {
    super(props)
    this.state = { count: props.start_count }
  }

  render() {
    return (
      <div>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click Me!
        </button>
      </div>
    )
  }
}

只看 class 部分的長度就頭皮發麻了,那麼 Functional 的呢?

// reuse defined prop type, thus skip
...

// then define function (generator)
const FuncWithState = (props: CompWithStateProps) => {
  const [count, setCount] = useState(props.start_count)
  return (
    <div>
      <button onClick={() => setCount(() => count + 1)}>
        Click Me!
      </button>
    </div>
  )
}

兩個完全等價的寫法,顯然後者輕鬆愉悅 OwO

useEffect: 多功能的 side effect 處理函式

對應 componentDidMountcomponentDidUpdate 以及 componentWillUnmount 方法。

透過直接呼叫 useEffect,傳入遇到上述前兩方法所需完成的 callback function。若 useEffect 內的 callback 回傳為一函式,則其等價於 componentWillUnmount,將在生命週期走入終點時被呼叫。

還是以前者為例,假設要跟去 count 更新 title,如果是 class 版的,利用 Javascript 的函式 assignment 可以簡化實作為以下:

updateTitle(): void {
  document.title = `You've clicked ${this.state.count} times`
}

componentDidMount = this.updateTitle
componentDidUpdate = this.updateTitle

不過 Functional 更勝一籌:

// inside function body
  ...
  const [count, setCount] = useState(props.start_count)
+ useEffect(() => {
+ document.title = `You've clicked ${count} times`
+ })
  return (
  ...

useEffect 被呼叫的時機為「相依變數受到更動」。要了解此,首先看該函式的簽名。

(alias) useEffect(effect: React.EffectCallback,
                  deps?: React.DependencyList | undefined): void

可見第二個參數 (dependency) 可選,根據該參數內容,函式的行為如下 (FN 表某 callback function)

undefined (無傳入) empty list stateful variables
效果 每次 render 皆呼叫 只呼叫一次 根據相依變數列表
用例 useEffect(FN) useEffect(FN, []) useEffect(FN, [var1, var2, ...])

Hook 的使用規則

Hook 必須屬於 Top level,也就是不可以在以下三種情況下於生成元素之函式內呼叫 useXXX 系列函式:

  1. Loop
    ​​​​// NG example
    ​​​​for (let i = 0; i < lim; ++i)
    ​​​​    useEffect(fn_list[i])
    
  2. Condition (if-else, switch)
    ​​​​// NG example
    ​​​​if (a == b)
    ​​​​    const [c, setC] = useStates(fn)
    
  3. Nested function
    ​​​​// NG example
    ​​​​() => { useEffect(fn) }
    

自訂 Hook

生成生成元素之函式的函式:)

比如想將同樣的 stateuseEffect 方法套用到兩種不同元素上,可以先定義生成者:

type IncDecProps = {
  start_count: number
}

const FuncWithIncDecState = (props: IncDecProps) => {
  const [count] = useState(props.start_count)
  useEffect(() => {
    document.title = `You've clicked ${count} times`
  })
  return count
}

接著應用來生成不同元素的版本。

const FuncInDiv = (props: IncDecProps) => {
  const count = FuncWithIncDecState(props)
  return <div>{count}</div>
}

const FuncInLabel = (props: IncDecProps) => {
  const count = FuncWithIncDecState(props)
  return <label>{count}</label>
}