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 propshigher-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 的 componentDidMountcomponentDidUpdate API。
    • 每次渲染結束,皆會 effect,包含第一次渲染。
    • 傳入 useEffect 內的 effect function 依 Javascript 的特性,每次都會新建,useEffect 依此來更新 funcrion component 內的 state。
    • 不同於 componentDidMountcomponentDidUpdateuseEffect 中的 effect 並不會阻塞瀏覽器更新畫面。如果需要同步的行為,可以使用 useLayoutEffect API。
      • 雖然為 async 的,但一定會在下次渲染前完成。
      • callback 的呼叫會在元件渲染完成後,因此會比 useLayoutEffect 晚執行。
    • 如果要呼叫定義在 function component 內的 function,則建議將此 function 移至 effect scope 內,此舉可以清楚界定此 effect 所需的 props 及 state。
  • cleanup

    • effect 的回傳 function 稱作 cleanup,類似 React class component 的 componentWillUnmount API。
    • 每次重新渲染前,皆會執行 cleanup,包含元件移除前。
  • 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
  • componentDidMountcomponentDidUpdatecomopnentWillUnmount:使用 useEffect
  • componentDidCatchgetDerivedStateFromErro:目前沒有相對應的 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。