# 我也要成為 React 大師
> 如同龍佬!
<!-- 注意,本文的撰寫由於作者討厭 Javascript,所以直接以 Typescript 的角度撰寫實作,並說明與 JS 的不同:) -->
## 環境建置
請參考[新主流環境建置](https://hackmd.io/@Eroiko/software-studio/https%3A%2F%2Fhackmd.io%2F%40Eroiko%2Fss-vite-react-ts-tailwind)筆記。
## React 基礎和 Class component
React 是 JS UI 函式庫,旨在建構單頁、行動裝置軟體。
### VirtualDOM
html 元素構成一樹,VirtualDOM 會批次透過比較當前 DOM 和實際渲染內容的差異,蒐集需要重新渲染的資料並更新之,增加動態渲染元素之效率。
#### 更新方法
由根走訪 VirtualDOM 樹並傳遞差異,決定哪些枝條要更新。
<!-- :::spoiler 從例子開始識 React Component
### 從例子開始識 React Component
定義 `Child` 和 `Parent` [元素](#React-Component),分別繼承 [`React.Component`](#React-Component) 且分別都有[狀態][state] `name` 和 `say`。
[state]: #state
```typescript
class Child extends React.Component {
state: { name: string; say: string }
constructor(props: { pn: string; ps: string }) {
super(props)
this.state = { name: 'Shiritai', say: '~~I hate JS~~' }
}
}
```
```typescript
class Parent extends React.Component {
state: { name: string; say: string }
constructor(props: Readonly<unknown>) {
super(props)
this.state = { name: 'Eroiko', say: 'Meow' }
}
}
```
只寫 Javascript 的讀者可能會很困惑,怎麼相較 Javascript 要寫這麼多東西!?這是因為...
1. 兩種元素的 `state` 要顯示聲明型別。因為其預設型別是 `Readonly<{}>`,若不調整型別宣告則會在存取 `state` 的成員時得到警告。
```typescript
this.state.name = 'Elolikon' // warning: wrong type
```
2. `Parent` 建構子中 `props` 型別未知,故設為 `Readonly<unknown>` 關閉型別不符的警告,符合傳入 `super` 建構子的型別是 `{} | Readonly<{}>` 的規定。至於建構子相關的細節可見 Lifecycle 章節。
親代節點想傳遞自己的 `name` 和 `say` 給子節點可以透過
::: -->
### React Component
React 本於所有 HTML 元素都以 JSX 語法表達並生成,所有元素都應繼承 `React.Component` 物件,該物件有兩重要成員:
:::info
今後「元素」表示繼承 `React.Component` 物件之物件。
:::
* `props`: <font color=#cd5c5c>**不可變**</font>成員,該成員下又有多個不可變成員。
其為元素建構子的參數,於 VirtualDOM 的生成中由親代節點傳遞至子節點,並由子節點持有參考。
:::danger
`props` 不可變是對「持有的節點」而言,也就是上述的子節點;**對親代節點可變**。
至於原因,是因為 `props` 是親代與子節點的溝通渠道,也就是親代節點對子節點的依賴注入,可用於實作「親代節點發生變化後傳遞至子節點比較差異、重新渲染」的邏輯。若子節點直接更改 `props`,親代節點並不會收到重新渲染的通知,引起不可預期的錯誤。
詳細可參見[本提問串](https://stackoverflow.com/questions/51435476/why-props-in-react-are-read-only)。
:::
<div id='state'></div>
* `state`: <font color=#cd5c5c>**可變**</font>成員。紀錄元素的狀態,其之下可以有多個可變成員。與一般成員不同的是,作為 React 稽查的對象,會觸發 [Lifecycle](#Lifecycle) 中 Updating 過程。
:::warning
`state` 雖說可變,但應透過 `React.Component::setState` 方法改變,以觸發生命週期相關函式,而非直接 assign,直接 assign 只發生在建構子。
:::
至於元素的方法?將與 Lifecycle 一併介紹。
### Lifecycle
所有元素皆有 Lifecycle,即可以用相同的有限狀態機描述。分為三階段,並以物件 `React.Component` 的形式包裝,所有元素都應繼承之。

* Mounting: 創建元素並插入 DOM
* Updating: 重新渲染元素
* Unmounting: 從 DOM 裡移除元素
上圖粗體表重要方法,以下分別介紹。
#### `render`
本方法繪製「一個」HTML 元素。如果想一次繪製很多個元素,需將其包為一個,至少輸出時只能有一個。既然決定元素的外觀,那<font color=#cd5c5c>**必然要實作**</font>。
回傳值為以下之一。
1. JSX element
2. Array & fragments
3. Protals
4. `string`, `number`s, `boolean`s, `null`
渲染除了建構時,通常發生在 `props` 或 `state` 改變時,透過分析這兩者的重新渲染元素。當某節點要重新渲染時,該節點以降所有子元素都應該重新渲染。
#### `constructor`
建構子,初始化物件成員,可不親自實作。但**若要實作必須呼叫 `super` 建構子**。
#### `componentDidMount`
建立 DOM node,將渲染好的元素加入 DOM tree,可在此時建立網路 request。
#### `componentDidUpdate`
可進行 `props` 和 `state` 改變的前後檢查,參數主要有二: `prevProps` 和 `prevState`,為走過 update 流程前的成員值。
#### `componentDidUnmount`
可拿來取消一些請求,比如元件準備離開 DOM tree 時若正在請求資料,可以取消請求。
## 主流 React 風格: Functional Component
### 流派的遞嬗
不管元素的複雜與否,渲染元素都需要使用上述較為繁雜的物件語法。注意到物件導向的一大特色就是...程式碼比較長 XD,這降低小元件的撰寫速度與可讀性,故新 React 引入本風格,將物件導向必要的成分拆解為數個函式,增加開發效率。
注意「函式」本身似乎是 stateless 的,不過得益於 Javascript 中函式為「[頭等函式](https://ithelp.ithome.com.tw/articles/10243104)」([First class function](https://zh.wikipedia.org/zh-tw/%E5%A4%B4%E7%AD%89%E5%87%BD%E6%95%B0)) 的特性,依然可以給函式夾帶變數與函式,稱為 Hooking。
### 直接渲染函式
> 對應 `render` 函式,不過有差異。
如果說 Class component 風格是「齊全的工具箱」,那 Functional component 便是「易於擴充的工具箱」。前者使用時我們得全面考慮物件的生命週期,並實作相對應的方法們。後者可以由簡入繁,從基本的顯示開始,需要什麼功能再透過 Hooking 擴充元素的功能。
直接渲染用的便是如此,[參考官方文件](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components/),以下可以快速生成一 `div` 元素。
```typescript
// 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 實作按鈕累加計數:
```typescript
// 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 的呢?
```typescript
// 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 處理函式
:::info
:::
> 對應 `componentDidMount`、`componentDidUpdate` 以及 `componentWillUnmount` 方法。
透過直接呼叫 `useEffect`,傳入遇到上述前兩方法所需完成的 callback function。若 `useEffect` 內的 callback 回傳為一函式,則其等價於 `componentWillUnmount`,將在生命週期走入終點時被呼叫。
還是以前者為例,假設要跟去 count 更新 title,如果是 class 版的,利用 Javascript 的函式 assignment 可以簡化實作為以下:
```typescript
updateTitle(): void {
document.title = `You've clicked ${this.state.count} times`
}
componentDidMount = this.updateTitle
componentDidUpdate = this.updateTitle
```
不過 Functional 更勝一籌:
```diff
// inside function body
...
const [count, setCount] = useState(props.start_count)
+ useEffect(() => {
+ document.title = `You've clicked ${count} times`
+ })
return (
...
```
`useEffect` 被呼叫的時機為「相依變數受到更動」。要了解此,首先看該函式的簽名。
```typescript
(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
```typescript
// NG example
for (let i = 0; i < lim; ++i)
useEffect(fn_list[i])
```
2. Condition (`if-else`, `switch`)
```typescript
// NG example
if (a == b)
const [c, setC] = useStates(fn)
```
3. Nested function
```typescript
// NG example
() => { useEffect(fn) }
```
### 自訂 Hook
> 生成生成元素之函式的函式:)
比如想將同樣的 `state` 與 `useEffect` 方法套用到兩種不同元素上,可以先定義生成者:
```typescript
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
}
```
接著應用來生成不同元素的版本。
```typescript
const FuncInDiv = (props: IncDecProps) => {
const count = FuncWithIncDecState(props)
return <div>{count}</div>
}
const FuncInLabel = (props: IncDecProps) => {
const count = FuncWithIncDecState(props)
return <label>{count}</label>
}
```