---
# System prepended metadata

title: React 讀書會 Session 5
tags: [React]

---

# 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>
        );
}

```
看到的結果
![](https://i.imgur.com/lNlCXPR.jpg)

符合預期，的確忠實印出保存在 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>
    )
  }
}
```

![](https://i.imgur.com/0bZjo4e.jpg)

使用 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>
        );
}

```
看一下結果
![](https://i.imgur.com/1IjSWaV.jpg)

這邊的行為就跟 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>
```

我們看看上面的例子跑出來的結果是什麼
![](https://i.imgur.com/uNjPRWg.jpg)

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

![](https://i.imgur.com/uTBzkJz.png)


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

![](https://i.imgur.com/9g37bBL.png)

這張圖主要有四種階段

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>
```
結果如下
![](https://i.imgur.com/y8pMomP.jpg)

這樣就會正常計算每一個 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 講到