React 16.8 | Hook
什麼是 Hook?
Hooks are functions that let you “hook into” React state and lifecycle features from function components.
- 不用宣告 Class,即可以使用 React state 及 lifecycle。
- 僅能用在 React function component。
- 也可以自己寫個 Hook 來覆用 stateful 邏輯。
function Component (props) {
//useState 為 hook
const [count, setCount] = useState(0);
return (
<div>
<p>You Clicked {count} times</p>
<button onClick={e => setCount(count++)}>
Click Me
</button>
</div>
)
}
Motivation
Stateful 的邏輯難以重用
在現行的 React 版本中,透過 render props 及 higher-order components 這兩種 Pattern 處理;但需要對元件進行重構,耗時費工。
Hook 可以讓覆用的 Stateful 邏輯抽離。
複雜元件的可讀性低
當元件變得複雜時,每個生命週期 function 內可能會包含複數個不相干的邏輯,造成閱讀困能。
Hook 可以用來拆分這些邏輯。
Class 對人、機來說都太難了
- Class 對新手來說太難理解,還有一堆 bind。
- 不用處理
this。 - Class 對於 Prepack 之類的預編譯工具來說難以優化。
Hook 規範
- 只能用在 React function component (除了自己寫的 Hook 外)。
- 不可以在 loop, condition, nested function 中呼叫。
- React 依據 function component 中呼叫 Hook 的順序去建立 Hook 間的關係,如果 function component 重新渲染後,Hook 有所增加(condition 中的 Hook),則 React 無法正確將前一次的 state 跟現在的 state 連結再一起。
useState Hook
type LazyInitState<S> = () => S
type Updater<S> = (prevState: S) => S
type StateSetter<S> = (state: S | Updater<S>) => void;
function useState<S>(initState: S | LazyInitStaet<A>): StateSetter<S>
- 只有在第一次渲染、呼叫 useState 時會創建 state 及更新的 method,故命名為
use,而非create。 - lazyInitState: 真的開始渲染的時候才得到值。
- 只有在 StateSetter 接收的數值更動後才會重新渲染。
Multiple State Variable
可以一次擁有多個 state variable:
function Todo() {
const [complete, setComplete] = useState(false);
const [time, setTime] = useState(Date.now());
const [content, setContent] = useState("");
return (
<div>
<h4>{content}</h4>
<span>{complete} | {time}</span>
</div>
)
}
也可以把所有需要的 state 放在 Object 中:
function Todo() {
const [todo, setTodo] = useState({
complete: false,
time: Date.now(),
content: ""
});
return (
<div>
<h4>{todo.content}</h4>
<span>{todo.complete} | {todo.time}</span>
</div>
)
}
跟 React class component setState 不同的是,每次呼叫 setTodo 為取代,而非與舊的 state 合併:
setTodo({complete: true})
//新的 state 則為:
todo = {
complete: true,
time: undefined,
content: undefined
}
useEffect Hook
type CleanUp = () => void;
function useEffect(effect: () => CleanUp?, dependency:Array<any>?);
-
effect:
- 傳入 useEffect 內的 function 稱作 effect 類似 React class component 的
componentDidMount、componentDidUpdateAPI。 - 每次渲染結束,皆會 effect,包含第一次渲染。
- 傳入
useEffect內的 effect function 依 Javascript 的特性,每次都會新建,useEffect依此來更新 funcrion component 內的 state。 - 不同於
componentDidMount及componentDidUpdate,useEffect中的 effect 並不會阻塞瀏覽器更新畫面。如果需要同步的行為,可以使用useLayoutEffectAPI。- 雖然為 async 的,但一定會在下次渲染前完成。
- callback 的呼叫會在元件渲染完成後,因此會比
useLayoutEffect晚執行。
- 如果要呼叫定義在 function component 內的 function,則建議將此 function 移至 effect scope 內,此舉可以清楚界定此 effect 所需的 props 及 state。
- 傳入 useEffect 內的 function 稱作 effect 類似 React class component 的
-
cleanup
- effect 的回傳 function 稱作 cleanup,類似 React class component 的
componentWillUnmountAPI。 - 每次重新渲染前,皆會執行 cleanup,包含元件移除前。
- effect 的回傳 function 稱作 cleanup,類似 React class component 的
-
dependency
- 用來作為 effect 是否執行的依據。
- 重新渲染時,React 會比較前後兩次渲染的 comparisons 內的值是否相同,如果相同的話則不執行。
- 如果 effect 中使用了 props 或 state 且有設定 dependency,則建議所有使用到的 props 及 state 皆包含在內。
function Counter () {
const [count, setCount] = useState(0);
useEffect(() => {
// 這個 function 內的邏輯每次渲染, 更新後都會執行, 包含第一次渲染結束時。
document.title = `You Click ${count} times`;
})
return (
<div>
<div>{count}</div>
<button onClick={() => setCount(count +1)}>
Click
</button>
</div>
)
};
Multiple Effects
可以呼叫多次 useEffect 來分離不相干的邏輯:
function ChatBoard() {
const [socket, setSocket] = useState(null);
useEffect(() => {
fetch("server/socket/getUserId").then(socket => setSocket(socket))
return () => socket.detach();
})
useEffect(() => {
document.title = `${socket.userName} in chat board`
});
}
Custom Hook
- 以此覆用 stateful 行為。
- 命名以
use開頭且內部呼叫 Hook 的 function 即為 custom Hook。 - 每次呼叫 Hook 時,Hook 內部的 state 皆為獨立的。
- 沒什麼用的範例:
function useCounter(countFrom: string = 0) { const [count, setCount] = useState(countFrom); const increasCount = () => setCount(count + 1); return [count, increasCount] }
其他 Hook
useContext Hook
function useContext(context: Context): Context.Consumer
在 React function component 內使用 Context。
useReducer Hook
type Reducer<S, A> = (state: S, action: A) => S;
type Dispatcher<A> = (action: A) => void;
type Initializar<S, V> = (initArg: V) => S;
function useReducer<S, A>(reducer: Reducer<S, A>, initState: S): [S, Dispatcher<A>]
function useReducer<S, A, V>(reducer: Reducer<S, A>, initState: V, init: Initializar<S, V>): [S, Dispatcher<A>]
功能類似 useState,只是更新時提供的不是新的 state,而是關於更新行為的描述,之後 Reducer 根據這組描述進行更新。
只有在 Reducer 回傳的值不同於當前 state 才會重新渲染。
useRef
效果等同於 createRef;唯一不同之處在於只產生於第一次渲染時,更新畫面時使用同一個 instance。
useMemo Hook
useCallback Hook
功能同 class component 的 shouldComponentUpdate API 及 useMemo
useLayoutEffect Hook
-
功能同
useEffect,只差在為 sync 的。 -
第一次渲染完執行的 effect 中如果有執行
setState(),會合併為一次渲染。
useImperativeHandle Hook
- 用來客製化暴露給父元件使用的 ref。
- 用來提供 api 給父元件呼叫。
- 必須搭配
forawrdRef使用。
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
console.log("called");
inputRef.current.focus();
}
}))
return (
<input ref={inputRef} {...props}/>
)
});
function ParentComponent() {
const fancyInputRef = useRef();
useEffect(() => fancyInputRef.current.focus())
return (
<div>
<FancyInput ref={fancyInputRef} />
</div>
)
}
Migration From Class Component
React.Component API
constructor:function 沒有constructor;如果需要 state,直接在 function scope 呼叫useState就可以了。shoudComponentUpdate:使用React.memo創建 function component。render:function component 本身可視為render。componentDidMount、componentDidUpdate、comopnentWillUnmount:使用useEffect。componentDidCatch、getDerivedStateFromErro:目前沒有相對應的 Hook 可用。getDerivedStateFromProps:所有東西都在同個 scope 內,所以不用特別處理。
用沒有 instance 的 variables?
可以透過 useRef 達成:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {}, 1000);
return () => clearInterval(intervalRef.current);
})
}
如何取得 previos props 或 previos state?
用 useRef:
function usePrevios(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
})
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<span>{count}</span>
<span>{prevCount}</span>
</div>
)
}
如何觀測 DOM node?
使用 callback ref。
- 只要 ref 綁定到不同的 DOM 就會被呼叫。
- 使用
useCallback且將空 Array 設為 depency 可以避免重新渲染觸發 callback。
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
在 effect 中呼叫定義在 effect scopr 外的 function?
- 把這個 function 移出 function component scope 避免其使用了 props 或 state。
- 此 function 為純計算用,沒有任何相依。
- 使用
useCallback包裹、限制 function。