# React 讀書會 Session 5
###### tags: `React`
### 元件每次渲染時 Props 與 State 都是獨立的
* 我們在 session 3 講到 useState 有提到元件每次渲染時 Props 與 State 都是獨立的,是無法改變的我們看以下的例子
```
import React, { useState, useEffect }from 'react';
export function Counter () {
const [count, setCount] = useState(0);
useEffect(()=>{
setTimeout(()=>{
console.log(`You clicked ${count} times`);
},3000);
});
return (
<div>
<button onClick={()=> setCount(v => v + 1)}>btn</button>
<div> {count} </div>
</div>
);
}
```
看到的結果

符合預期,的確忠實印出保存在 Counter 每一次渲染時的狀態
但有時候我在每次渲染時希望取得某一次 count 的狀態又或是 count 未來最新的狀態時又該怎麼做?
我們看一下在 React Class Component 裡面我們用同樣的方式來實作上面的效果
```
import React, {Component} from "react";
import ReactDOM from "react-dom";
export class Counter extends Component {
state = {
count: 0
};
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({
count: this.state.count + 1
})}>
btn
</button>
</div>
)
}
}
```

使用 class component 實作時跟你想的不一樣對吧?
每次印出的 count 是最新的狀態
因為這裡的 count 是指向 class component 的實體的成員,並不是 function component 每次 render closure 本身
所以印出 count 是指向同一個物件同一個記憶體,也就是最新的狀態
那麼,我們在 function component 是否能達成類似的效果呢?
### useRef
根據官方的說法
useRef 回傳一個 mutable 的 ref object,.current 屬性被初始為傳入的參數(initialValue)。
回傳的 object 在 component 的生命週期將保持不變。
看不懂上面官方說法沒關係
白話來說
就是讓 function component 的生命週期裡這個物件都是同一個 (同一個記憶體)
不會像是 useState 一樣每次渲染都是不同的物件
然後這個物件下面有個屬性 current 是可以拿來隨時存取的你要用的狀態
其實就是要模擬 class component 中的 instance field
我們用 useRef 來試試看效果
```
import React, { useState, useEffect, useRef }from 'react';
export function Counter () {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(()=>{
countRef.current = count;
setTimeout(()=>{
console.log(`You clicked ${countRef.current} times`);
},3000);
});
return (
<div>
<button onClick={()=> setCount(v => v + 1)}>btn</button>
<div> {count} </div>
</div>
);
}
```
看一下結果

這邊的行為就跟 class component 裡的行為一致了
透過 current 讓我們可以獲取指定的狀態
不過你可能會問
那我在 function component 外面去定義一個變數不是也是一樣效果?
```
import React, { useState, useEffect, useRef }from 'react';
let countRef = 0;
export function Counter () {
const [count, setCount] = useState(0);
countRef = count;
useEffect(()=>{
countRef = count;
setTimeout(()=>{
console.log(`You clicked ${countRef} times`);
},3000);
});
return (
<div>
<button onClick={()=> setCount(v => v + 1)}>btn</button>
<div> {count} </div>
</div>
);
}
```
這樣寫的確會是一樣的效果,但是當我們在多個地方使用到這個元件時,因為這個 countRef 變數是在 function 外部
在不同地方的元件都會共同使用到這個變數,然後就會互改造成錯誤
### useRef vs useState
一樣都是讓 Function Component 擁有狀態
同樣模擬 class 裡 instance field 的效果
那 useRef 與 useState 有什麼不同?
最大的不同是
useRef 改變值並不會觸發 React 去呼叫重新渲染
useRef 在渲染時都會給你同一個 ref object (同一個記憶體位置)
具體來說,React 元件中想要定義一些變數
但又不想這些變數改變時就像 useState 會重新呼叫被渲染
這時就很適合使用 useRef
### useRef 使用規範與使用情境
由於建構 useRef 的 Ref 物件是貫穿整個元件的生命週期,在任何地方都可以做修改
最常做的就是計算元件的每做一次 lifecycle 的次數 (這邊不講渲染次數的原因後面會講)
```
import React, { useState, useEffect, useRef }from 'react';
export function Counter () {
const countRef = useRef(0);
return <>{++ countRef.current}</> // 直接在渲染階段裡修改值
```
但實際上這種方式並不是 React 建議使用 Ref 的規範
React 建議我們避免在 Render 函數中直接修改 Ref
為什麼這麼說,我們把上面的範例修改一下,刻意按按鈕測試次數
```
import React, { useState, useEffect, useRef, useReducer }from 'react';
export function Counter () {
const countRef = useRef(0);
const render = useReducer(state => !state, false)[1]; // 強制Render用的,還不會useReducer沒關係先不管他,後面章節會講
return <div>
<button onClick={render}>btn</button>
<>{ console.log(++ countRef.current)}</>
</div>
```
我們看看上面的例子跑出來的結果是什麼

跟我們預期的完全不同對吧?
我們發現每按一次按鈕就渲染了兩遍 (useRef 很忠實的記錄你渲染次數)
也跟我們想像應該只要渲染一遍的行為不一樣
但這個問題本身並不在於 useRef
而是在於 React 在 16.x 版之後預設加入 React.StrictMode 嚴謹模式
我們看看官網的對於 React.StrictMode 模式的說明

在嚴謹模式下的開發環境裡 React 會幫你執行兩次 render function (但 useEffect 一樣只執行一次)
用意在讓你發現有沒有不正常的 side effect那這問題要怎麼解決呢?(當然最快就是拔掉嚴謹模式)
我們先看看 FunctionComponent 的生命週期

這張圖主要有四種階段
Render Phase : Run lazy initializers 與 Render
Reconciliation Phase : React Updates DOM
Layout Phase : Cleanup LayoutEffectsRun LayoutEffects
Commit Phase : Cleanup EffectsRun Effects
這個 Flow 的前兩個綠色階段
Run lazy initializers 與 Render
是屬於 Render Phase
這個階段是不允許做 Side Effects 的,也就是寫有副作用的程式,修改 Ref 就是屬於副作用的操作
這是因為 React 引擎在這個階段隨時可能取消或是重做或是像嚴謹模式執行兩遍
React 建議只有允許副作用的的階段做這件事
只有一個情況是例外的就是 lazy initializers (懶初始化)
```
import React, { useRef } from 'react';
export function Counter () {
const countRef = useRef(null);
if(countRef.current === null){
countRef.current = 0;
}
return <div></div>
```
懶初始的情況下,副作用最多執行一次,而且僅用於初始賦值
這種行為是允許的的特例
如果要讓前面講的計算元件 lifecycle 次數正常運行
我們就必須將有 sideEffect 的修改放到 useEffect 裡去執行
```
import React, { useState, useEffect, useRef, useReducer }from 'react';
export function Counter () {
const countRef = useRef(0);
const render = useReducer(state => !state, false)[1];
useEffect(()=>{
countRef.current ++ ;
});
return <div>
<button onClick={render}>btn</button>
<>{ console.log(countRef.current)}</>
</div>
```
結果如下

這樣就會正常計算每一個 lifecycle 的次數了
另外 useRef 也常被用來參照 DOM 實體
因為每一次渲染都會建出一個新的 DOM
將 DOM 上的 ref 屬性參照給 useRef 實體
透過 useRef 實體可以拿到當下 render 狀態實際的 DOM 物件
並且能夠去控制他們
例如
```
import React, { useRef } from 'react';
export function Counter () {
const inputRef = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputRef.current.focus();
};
return <div>
<input ref={inputRef}></input>
<button onClick={onButtonClick}>Focus the input</button>
</div>
```
還有一個情境父元件想要存取子元件裡面的 DOM 元素時該怎麼做?
實作概念也很簡單使用情境如下
```
export const Child = ()=>{
return <>
<input/>
</>
}
export const Parent = ()=>{
return <>
<Child />
<button>focus child</button>
</>
}
```
以上面的範例我希望按下 Parent 元件裡的 button 能讓 Child 元件裡的 input 去 focus
這該怎麼做呢?
首先
Parent 元件裡先定義要存取 Child 的變數
我們使用 useRef 來參照 Child 裡面的元素
把 useRef 當成 props 傳進 Child 裡
```
export const Parent = ()=>{
const childEl = useRef(null);
return <>
<Child inputRef={childEl}/>
<button>focus child</button>
</>
}
```
然後將 Child 元件帶入 props
```
export const Child = ({inputRef})=>{
return <>
<input ref={inputRef}/>
</>
};
```
這樣子就能從父元件直接控制子元件裡的 DOM 元素了
```
export const Parent = ()=>{
const childEl = useRef(null);
const onFocus = () => {
childEl.current.focus(); // childEl.current就是Child元件裡的input元素
}
return <>
<Child inputRef={childEl}/>
<button onClick={onFocus}>focus child</button>
</>
}
```
那如果我希望像是在 class component 那樣使用 ref 去取得整個 Child 呢?
預設上,你不能在 function component 上使用 ref,因為它們沒有 instance
這方式只能在 class component 使用 (使用 createRef 方法)
那在 function component 有沒有方式能夠像在 class component 時使用 ref?
### forwardRef
首先將原本的 Parent 元件裡的 Child 元件上的 prop inputRef 改成使用 ref
```
export const Parent = ()=>{
const childEl = useRef(null);
const onFocus = () => {
childEl.current.focus();
}
return <>
<Child ref={childEl}/>
<button onClick={onFocus}>focus child</button>
</>
}
```
修改 Child 元件
Child 元件必須使用 forwardRef 方法給大家使用
forwardRef 是 React 的 HOC (Higher-Order Components)
forwardRef 帶的參數就是 function component 本身
```
export const Child = forwardRef(()=>{
return <>
<input />
</>
});
```
Child 元件本身也要帶兩個參數給父元件 inputData 用
第一個參數就是父元件塞過來的 inputData
第二個參數就是 ref 本身
我們原本父元件帶來的 useRef 是使用 inputData 帶進來的,現在直接使用第二參數 ref
```
export const Child = forwardRef((prop, ref)=>{
return <>
<input ref={ref}/>
</>
});
```
當然你可以直接把 ref 參數指定給 input 元素這樣父元件就能完整控制子元件裡 input 所有的功能
```
export const Parent = ()=>{
const childEl = useRef(null);
const onFocus = () => {
childEl.current.focus();
}
return <>
<Child ref={childEl}/>
<button onClick={onFocus}>focus child</button>
</>
}
```
不過這樣的寫法跟原本父元件 useRef 直接帶 props 進來子元件的方式沒什麼不同
我需要定義 Child 自己的方法給父元件使用
或是限制父元件來取用子元件裡其他 ref
這時該怎麼做?
### useImperativeHandle
React 為 function component 提供 useImperativeHandle 鉤子
讓使用 ref 時能向父元件暴露自定義的 instance 值
```
export const Child = forwardRef((prop, ref)=>{
const inputRef = useRef(null);
// 定義給父元件控制的方法
useImperativeHandle(ref, ()=>({
lockOn:()=>{
inputRef.current.focus();
}
}));
return <>
<input ref={inputRef}/>
</>
});
export const Parent = ()=>{
const childEl = useRef(null);
const onFocus = () => {
childEl.current.lockOn(); // 使用子元件定義給父元件的方法
}
return <>
<Child ref={childEl}/>
<button onClick={onFocus}>focus child</button>
</>
}
```
這樣父元件就能使用子元件定義的方法了 (類似 instance 成員方法)
而使用 useImperativeHandle 方式基本上必須與 forwardRef 搭配使用
以上大概就是使用 useRef 會用到的情境
還有另外一個情境是使用 callBack 當成 ref
這個會在下一個章節 useMemo, useCallBack 講到