學習React就要學習他現在主流的寫法Hook,學習Hook的好處很多,可以在function中使用state跟一些類似生命週期的React方法,重點是學習的成本相對較低,相較於class的架構,比較簡單容易上手,可讀性也高
內容目錄
useState
這是可以同時存儲值以及觸發渲染機制的功能,一些規則是
- 宣告的時候要同時設定兩個值,一個是state本身,另一個是更新state的函式
- 更新的函式通常是set+第一個設定變數
- useState()裡面參數為”初始值”
首先先簡單寫一個計數器,在沒有加useState的情況下,會發生什麼
- 先設定一個APP的function組件
- return一個h1跟button
- button放一個numAdd的點擊事件,事件可以增加num的數字
我們預期會點擊後更新畫面,但實際上只有num被改變,畫面沒有被渲染
import React, { useState } from "react";
const App: React.FC = () => {
let num = 0;
const numAdd = () => {
num++;
};
console.log("num", num);
return (
<>
<h1>計數器{num}</h1>
<button onClick={numAdd}> +1 </button>
</>
);
};
export default App;
};

那是因為需要觸發渲染機制,才會改變畫面,那我們就要使用useState來讓變數的更動,觸發渲染機制
- 把useState給import進來
- 設定初始的useState值
- 將numAdd的方法修改為State修改值的方法
就可以發現,點擊後的數字修改,也會讓畫面上的數字跟著修改
import React, { useState } from "react";
const App: React.FC = () => {
// let num = 0;
const [num, SetNum] = useState(0);
const numAdd = () => {
SetNum(num + 1);
};
console.log("num", num);
return (
<>
<h1>計數器{num}</h1>
<button onClick={numAdd}> +1 </button>
</>
);
};
export default App;

這邊再提到一個重要的觀念變數渲染的作用範圍,實務上比較容易遇到,至於影片*中提到的批量更新的部分就不記錄了
※影片是指 布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI
的課程
這裡拿剛剛的例子,在numAdd這裡面再新增一次+1,我們期望可以點擊一次,觸發兩次,但實際上,他只會運作一次
const App: React.FC = () => {
// let num = 0;
const [num, SetNum] = useState(0);
const numAdd = () => {
SetNum(num + 1);
SetNum(num + 1);
};
console.log("num", num);
return (
<>
<h1>計數器{num}</h1>
<button onClick={numAdd}> +1 </button>
</>
);
};
export default App;

這裡把他修改為取得上一個數字,並+1回傳,這樣得情況下,才可以達成我們要的在同一個作用下渲染兩次
import React, { useState } from "react";
const App: React.FC = () => {
// let num = 0;
const [num, SetNum] = useState(0);
const numAdd = () => {
const newNum = (prev: number): number => {
return prev + 1;
};
SetNum(newNum);
SetNum(newNum);
};
console.log("num", num);
return (
<>
<h1>計數器{num}</h1>
<button onClick={numAdd}> +1 </button>
</>
);
};
export default App;

useEffect
跟useState很像,當改變設定的值時,會觸發渲染,但同時也會觸發本身帶的方法,而不是像useState改變變數
- 宣告時,設定時為一個方法
- 參數一是自帶一個方法,當觸發時會被執行
- 參數二是一個陣列,裡面放要監聽的變數
繼續拿之前的計數器,加上奇偶數的判斷,就要在useEffect中加上加入這些判斷式
- 將useEffect給import進來
- 先設定儲存奇偶數的state
- return一個p標籤來顯示奇偶數
- 宣告一個useEffect並且將num數字變化給放入參數2
所以每當數字改變時,就會判斷是奇、偶數
import React, { useState, useEffect } from "react";
const App = () => {
const [num, setNum] = useState(0);
const [test, settest] = useState("偶數");
useEffect(() => {
if (num % 2 === 0) {
settest("偶數");
} else {
settest("奇數");
}
}, [num]);
const numAdd = () => {
setNum(num + 1);
};
return (
<>
<h1>計數器:{num}</h1>
<button onClick={numAdd}>+1</button>
<p>{test}</p>
</>
);
};
export default App;


模擬生命週期componentDidMount()
要知道fnnction是沒有生命週期的,用這方式,是讓function可以透過useEffect來模擬生命週期的感覺,那就是將參數二的陣列不要放值即可,這樣就只有一開始渲染時會執行一次,這裡面可以放一開始渲染時要抓的內容(一些需要抓後端的內容)
import React, { useState, useEffect } from "react";
const App = () => {
useEffect(() => {
console.log("hello");
}, []);
return (
<>
</>
);
};
export default App;

retrun功能
useEffect的return呢,重新渲染useEffect本身之前,會執行的一個動作,而是重新渲染,那就不包含第一次渲染
以下範例是,點擊按鈕觸發useState的重新渲染,並且觀察重新渲染的effect是return比原本的執行內容還要先跑
結果就是第一次渲染沒有執行return,重新渲染後,就會開始執行return
import React, { useState, useEffect } from "react";
const App = () => {
const [num, setNum] = useState(0);
console.log("num", num);
useEffect(() => {
if (num == 0) {
console.log("第一次useEffect執行");
} else {
console.log("useEffect執行");
}
return () => {
console.log("useEffect執行return");
};
}, [num]);
return (
<>
<button
onClick={() => {
setNum(num + 1);
}}
>
重新渲染
</button>
</>
);
};
export default App;

useRef
有點類似useState,但他不涉及觸發渲染,也就是說只有保存值的功能,聽起來一般變數也能做到,但實際變數在使用時,可能會因為重新渲染被覆蓋掉
我們設定一個變數跟useState,兩個同時被點擊後會+1,然後重新渲染被印出來,但得到的結果num都是0,是因為每次num都被重新渲染後,重新宣告為0,而且useState每次都會觸發渲染,我們並不想要渲染效果
import React, { useRef, useState } from "react";
const App: React.FC = () => {
let num: number = 0;
const [numState, setNumState] = useState(0);
console.log("num", num, "numState", numState);
const clickNum = () => {
num++;
setNumState(numState + 1);
};
return (
<>
<button onClick={clickNum}> + 1 </button>
</>
);
};
export default App;

換成用useRef的時候,就是不會觸發渲染,可以在裡面繼續運算著+1,直到5的時候觸發useState渲染之後,也不會歸零,一樣可以持續剛剛的數字繼續往下加
import React, { useRef, useState } from "react";
const App: React.FC = () => {
const numRef = useRef(0);
const [hidden, setHidden] = useState(false);
const clickNum = () => {
numRef.current++;
console.log(numRef.current);
if (numRef.current == 5) setHidden(true);
};
return (
<>
<button onClick={clickNum}> + 1 </button>
{hidden && <p>隱藏區塊</p>}
</>
);
};
export default App;
useContext
之前學過的props是把父組件的值,傳遞到子組件上面去使用,但如果今天組件非常多層的時候,最上層要傳到最下層,只使用props的時候,一層一層傳下去造成非常麻煩的過程,程式碼也很亂,這時候就可以使用useContext
- 首先把useContext跟createContext從react給import進來
- 先建立createContext,並且給予他一個預設值
- createContext的預設值是,當拿到這個值,卻不是在本身子組件內,就會達到預設值
- 建立一個組件,並且參數為children(預設的props)
- retrun的部分是將上面創立的createContext有預設Provider的組件
- value值則是真正會被傳遞下去的值,如果不是子組件拿到createContext就是拿到預設
- 下面再建立一個自訂義的hook把useContext設定好,是用來取的值用的
※2023/02/23更新:children必須賦予型態
import React, { useContext, createContext, useState } from "react";
const numDefult:number =0
const BtnContext = createContext(numDefult);
type Props ={
children: React.ReactNode; //👈 children prop typr
};
export const BtnProvider = (prop:Props ) => {
const [num, setNum] = useState(1);
const clickAdd=()=>{
setNum(num+1)
}
return (
<>
<BtnContext.Provider value={num}>
{prop.children}
<button onClick={clickAdd}>+1</button>
</BtnContext.Provider>
</>
);
};
export const useBtnContext = () => {
return useContext(BtnContext);
};
要使用剛剛設定的useContext先回到原本有很多層組件的檔案,這邊設定app包app1包app2,所以我想讓app2拿到最上層的值
- 將剛剛設定的組件還有hook給import進來
- 把設定好的BtnProvider組件包覆在要傳遞的子組件(app1)
- 這樣app1跟app2都是useContext的範圍
- 那這裡使用設定的hook,就能取得我需要的值
import React from "react";
import { useBtnContext, BtnProvider } from "./context";
const APP2: React.FC = () => {
const num = useBtnContext();
return (
<>
<p>這是app2</p>
<p>現在數字為 {num}</p>
</>
);
};
const APP1: React.FC = () => {
return (
<>
<p>這是app1</p>
<APP2 />
</>
);
};
const App: React.FC = () => {
return (
<>
<BtnProvider>
<APP1 />
</BtnProvider>
</>
);
};
export default App;
複習JS的比較
我們透過”===”可以比較兩者是否為同等型態及同樣的值,但對於物件、陣列、函式來說,比的是”記憶體位置”,也就是他們是不是完全同一個地方出來的
從下面這幾行程式碼知道,物件、陣列、函式比較的並非是長的一不一樣,而是存取的位置在哪裡
//true
console.log("數字比較", 1000 === 1000);
//false
console.log("陣列比較", [] === []);
//false
console.log("物件比較", {} === {});
//false
console.log("函式比較", (() => {}) === (() => {}));

useMemo
是用來記憶一個值,而記憶本身是為了不做無謂的重新渲染,以及一些行為,減少效能的消耗,讓速度提升
宣告方式
- 先import進來
- 宣告時,參數一是一個帶有回傳的function,如果不帶回傳就是undefind
- 宣告時,參數一是跟useEffect一樣,帶入監聽的內容
從這個範例可以看到,如果我們一般宣告useEffect,參數二沒監聽內容,就只會執行一次,不管重新渲染幾次都一樣
import React, { useEffect, useState, useMemo, useCallback } from "react";
const App = () => {
const [value, setValue] = useState(false);
console.log("重新渲染");
useEffect(() => {
console.log("載入後端資料");
}, []);
return (
<>
<button
onClick={() => {
setValue(!value);
}}
>
重新渲染
</button>
</>
);
};
export default App;

但如果今天我們需要監聽每個物件,來更新後端的資料,就會造成一旦不相干的畫面更新,也會影響到useEffect的內容,因為上述物件比較,每次物件的位置都被重新渲染時重新設定了
import React, { useEffect, useState, useMemo, useCallback } from "react";
const App = () => {
const [value, setValue] = useState(false);
console.log("重新渲染");
const relodding = {name :'hank'};
useEffect(() => {
console.log("載入後端資料");
}, []);
return (
<>
<button
onClick={() => {
setValue(!value);
}}
>
重新渲染
</button>
</>
);
};

這時候使用useMemo來記憶物件的內容,並且只要宣告後,物件本身的內容沒有改變,就不會useEffect重新被渲染
import React, { useEffect, useState, useMemo, useCallback } from "react";
const App = () => {
const [value, setValue] = useState(false);
console.log("重新渲染");
// const relodding = { name: "hank" };
const relodding = useMemo(() => {
return { name: "hank" };
}, []);
useEffect(() => {
console.log("載入後端資料");
}, [relodding]);
return (
<>
<button
onClick={() => {
setValue(!value);
}}
>
重新渲染
</button>
</>
);
};
export default App;
useCallback
功能跟剛剛useMemo差不多,只是他是自帶function,就不能做其他事,那useMemo則是回傳一個值,但同樣可以做到回傳function
宣告的話
- 先import進來
- 宣告時,參數一是只能放入function,並且就是自帶回傳這個function
- 宣告時,參數一是跟useEffect一樣,帶入監聽的內容
先從剛剛的例子改成Memo回傳function
import React, { useEffect, useState, useMemo, useCallback } from "react";
const App = () => {
const [value, setValue] = useState(false);
console.log("重新渲染");
const relodding = useMemo(() => {
return ()=>{}
}, []);
useEffect(() => {
console.log("載入後端資料");
}, [relodding]);
return (
<>
<button
onClick={() => {
setValue(!value);
}}
>
重新渲染
</button>
</>
);
};
export default App;
那其實useCallback可以做到一樣的效果,只是他自己本身就是帶fucntion,所以不會再回傳function
import React, { useEffect, useState, useMemo, useCallback } from "react";
const App = () => {
const [value, setValue] = useState(false);
console.log("重新渲染");
const relodding = useCallback(() => {}, []);
useEffect(() => {
console.log("載入後端資料");
}, [relodding]);
return (
<>
<button
onClick={() => {
setValue(!value);
}}
>
重新渲染
</button>
</>
);
};
export default App;
memo
當組件有上下層關係的時候,只要父層重新渲染,子層的組件也會重新渲染,但其實變更的內容只有父層的時候,子層的渲染就是多餘的,避免這種浪費效能的狀況,就可以使用memo
宣告方式
- 先import進來
- 直接將組件等號後面的部分全部都包起來
從範例中可以看到,只要點擊父層的按鈕,重新渲染後,子層的組件也會被重新渲染
import React, { useState, useMemo, memo } from "react";
const App1: React.FC = () => {
console.log("子層渲染");
return (
<>
<p>子層</p>
</>
);
};
const App: React.FC = () => {
console.log("父層渲染");
const [relodding, setRelodding] = useState(false);
return (
<>
<App1 />
<button
onClick={() => {
setRelodding(!relodding);
console.log("---點擊重新渲染---");
}}
>
重新渲染
</button>
</>
);
};
export default App;

加入meno就不會有這個問題,他會判斷是否子組件有發生變動,有的話才會進行渲染
import React, { useState, useMemo, memo } from "react";
const App1: React.FC = memo(() => {
console.log("子層渲染");
return (
<>
<p>子層</p>
</>
);
});
const App: React.FC = () => {
console.log("父層渲染");
const [relodding, setRelodding] = useState(false);
return (
<>
<App1 />
<button
onClick={() => {
setRelodding(!relodding);
console.log("---點擊重新渲染---");
}}
>
重新渲染
</button>
</>
);
};
export default App;

加入props之後,當數字發生變化,也代表子組件變化,所以當變化完之後,不在變化時,就不會繼續渲染,只會渲染發生變化的那次
import React, { useState, useMemo, memo } from "react";
type prop = {
num: number;
};
const App1: React.FC<prop> = memo(({ num }) => {
console.log("子層渲染");
return (
<>
<p>子層</p>
<p>數字{num}</p>
</>
);
});
const App: React.FC = () => {
console.log("父層渲染");
const [relodding, setRelodding] = useState(false);
const [num, setNum] = useState(0);
return (
<>
<App1 num={num} />
<button
onClick={() => {
setRelodding(!relodding);
console.log("---點擊重新渲染---");
}}
>
重新渲染
</button>
<button
onClick={() => {
setNum(100);
console.log("---點擊數字---");
}}
>
數字
</button>
</>
);
};
export default App;

memo結合useMemo的使用
從這個範例中,我們可以知道當傳送過去的東西變成物件之後,又開始不斷刷新子組件,問題還是在於,物件的記憶體位置發生變化,所以這時候就可以加入useMemo來解決這個問題
import React, { useState, useMemo, memo } from "react";
type prop = {
num: number;
obj: { name: string };
};
const App1: React.FC<prop> = memo(({ num, obj }) => {
console.log("子層渲染");
return (
<>
<p>子層</p>
<p>數字{num}</p>
<p>名子{obj.name}</p>
</>
);
});
const App: React.FC = () => {
console.log("父層渲染");
const [relodding, setRelodding] = useState(false);
const [num, setNum] = useState(0);
const [obj, setObj] = useState({ name: "?" });
return (
<>
<App1 num={num} obj={obj} />
<button
onClick={() => {
setRelodding(!relodding);
console.log("---點擊重新渲染---");
}}
>
重新渲染
</button>
<button
onClick={() => {
setNum(100);
console.log("---點擊數字---");
}}
>
數字
</button>
<button
onClick={() => {
setObj({ name: "hank" });
console.log("---點擊姓名---");
}}
>
姓名
</button>
</>
);
};
export default App;
中間再加入useMemo的回傳,這樣就可以判定是不是有發生值的變動,而不是記憶體位置的變動
import React, { useState, useMemo, memo } from "react";
type prop = {
num: number;
obj: { name: string };
};
const App1: React.FC<prop> = memo(({ num, obj }) => {
console.log("子層渲染");
return (
<>
<p>子層</p>
<p>數字{num}</p>
<p>名子{obj.name}</p>
</>
);
});
const App: React.FC = () => {
console.log("父層渲染");
const [relodding, setRelodding] = useState(false);
const [num, setNum] = useState(0);
const [obj, setObj] = useState({ name: "?" });
const objMemo = useMemo(() => {
return { name: "hank" };
}, []);
return (
<>
<App1 num={num} obj={obj} />
<button
onClick={() => {
setRelodding(!relodding);
console.log("---點擊重新渲染---");
}}
>
重新渲染
</button>
<button
onClick={() => {
setNum(100);
console.log("---點擊數字---");
}}
>
數字
</button>
<button
onClick={() => {
setObj(objMemo);
console.log("---點擊姓名---");
}}
>
姓名
</button>
</>
);
};
export default App;

結論
今天持續學習布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI,基本上React Hook的觀念我之前有學過一輪,但完全忘記怎麼使用,畢竟當初學也沒做紀錄,也一段時間都沒接觸React,但透過課程學習之後,確實讓我印象深刻的是,布魯斯會說明每個Hook為什麼會被這樣使用及設計,不單單只是教怎麼使用而已,而是讓學的能夠理解原理,更能清楚使用的時機,以及跟過去存在的比較,讓觀念釐清之後,比較好記憶跟使用,所以比起之前看書自學,這次一輪的學習讓我對這些Hook的用法跟觀念都更上一層
