React Hooks 實作民調產生器

DEBUG
LT Lab
Published in
8 min readJul 23, 2019

Hooks 在 React 16.8 有下列幾種 API,自己常用的基本 Hooks 有 useState 、useEffect 及 useRef。這次透過實作練習,來了解其他 Hooks 的應用吧。

source: https://reactjs.org/docs/hooks-reference.html

藍色政黨初選民調結束真的是幾家歡樂幾家愁,這裡不談政治,來談談姓氏的喜好程度,百家姓真的太多了,就隨機抽了三個韓、郭、朱,姓氏排序是依照筆畫多到少。

首先會建立候選姓氏的元件,該元件會有姓氏、當前票數及增票與減票按鈕,另一元件為提交民調最後勝出結果的按鈕與文字,接下來就進入正題。

首先產生一個 html 樣版及程式的入口位置 index.tsx

useState

首先出場的是 useState,建立候選姓氏元件, state 紀錄候選姓氏的票數及更新後的票數(投票),state(票數) 為 immutable,如需變動須透過第二個參數(投票),去改變state(票數)的值,相當於 Class Component 中的 setState。state 值改變會觸發 re-render。

useEffect

早期寫 React 都會以 Class Component 為主,換到 Hooks 會困在 useEffect 只能在 Functional Component 定義一次的觀念,其實不是,useEffect 有兩個參數,第一個參數相當於 componentDidMount 及 componentDidUpdate,及一個 return 值,執行相當於 componentWillUnmount 的事件,第二個參數,為一個陣列非必須,傳入需要變更的值(state 、 props) ,更新後需要做的 Side Effect 即觸發 componentDidUpdate 事件。定義多個 useEffect 的需求,在於你 state 或 props 的連動 Side Effect 的關係。

在不設定第二參數下,默許所有變動執行 Side Effect,如果設為空值,表示都不執行 Side Effect。陣列有值的情況,就會依照設定的值異動,做Side Effect。

以下,針對整個畫面包了一個 Click 事件,當點擊畫面任一個地方,會觸發 render 值加一,查看左下角的 console 可以了解生命週期之變化。每個 useEffect 都會執行 componentDidMount,useEffect 第二個參數設定攸關 componentDidUpdate、componentWillUnmount 執行與否。

useLayoutEffect

上述談到 useEffect ,應該有人會好奇 Hooks 中的 useLayoutEffect,官方文件是建議先使用 useEffect 如有問題才用 useLayoutEffect,自己的使用時機如果需要調用 DOM 元素時,我會在 useLayoutEffect 實作。

以下為官方對於 useLayoutEffect 一些建議。

Tip

If you’re migrating code from a class component, note useLayoutEffect fires in the same phase as componentDidMount and componentDidUpdate. However, we recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem.

If you use server rendering, keep in mind that neither useLayoutEffect nor useEffectcan run until the JavaScript is downloaded. This is why React warns when a server-rendered component contains useLayoutEffect. To fix this, either move that logic to useEffect (if it isn’t necessary for the first render), or delay showing that component until after the client renders (if the HTML looks broken until useLayoutEffect runs).

To exclude a component that needs layout effects from the server-rendered HTML, render it conditionally with showChild && <Child /> and defer showing it with useEffect(() => { setShowChild(true); }, []). This way, the UI doesn’t appear broken before hydration.

useRef

useEffect 第一個參數包了 componentDidMount 及 componentDidUpdate,那如何判斷第一次是要 componentDidMount 還是 componentDidUpdate,這邊會使用到 useRef,初始值給 false,執行 componentDidMount 事件後變更為 true,useRef 是 mutable,取值時需調用 current 屬性,該值變動並不會觸發 re-render,與useState 不同,一些背後邏輯處理不須渲染畫面或者是要對 DOM 做操作可以使用 useRef。

const didMountRef = React.useRef(false);

以下對票數結果做緩存,下面會談到 Context 也有處理票數的數據,這邊會在處理票數的目的是處理 Context 的異動做無意義的 re-render。

const voteRef = React.useRef([
{ name: "韓", result: 0 },
{ name: "郭", result: 0 },
{ name: "朱", result: 0 }
]);const totalRef = React.useRef(0);

透過 useRef 取 DOM 元素,取的元素的當前位置並作移動。

const btnWrapRef = React.useRef<HTMLDivElement | null>(null);const handleClickSubmit = () => {
if (btnWrapRef.current) {
let offset = btnWrapRef.current.offsetTop;
window.scrollTo(0, offset);
}
}return (
<div ref={btnWrapRef}> ... </div>
)

useContext

當有父層參數要逐一傳到各個子層時,Context 的使用時機就來了,如何來實作跨組間的溝通,先實例化 React.createContext,並在父元素上,包覆 ContextStore.Provider,該屬性的 value 值即為要傳遞之參數或方法。

子元素可以透過 useContext 取得父元素提供的屬性或方法。

調用 useContext 的組件,會因為 context 的內的值變動而做 re-render的狀況,導致很多不必要的渲染,優化的部分可以參考此篇(Preventing rerenders with React.memo and useContext hook.)。

Context 處理的參數,個人票數統計,所有候選姓氏的當前票數。個人票數計算,主要傳遞 useReducer 的 dispatch 方法。總票數,即為三個候選姓氏的票數總和。

const ContextStore = React.createContext({
個人票數總計: {},
個人票數計算: null,
總票數: 0
});

useReducer

將背後統計票數的邏輯集中管理,建立一支 reducer.tsx,將票數寫入 store 裡頭。

useReducer 需傳入兩個參數,一個是要處理之動作並觸發 state 更新,另一個是預設之 state。回傳的資料,為當前的 state 值及 dispatch 的方法。

const [state, dispatch] = React.useReducer(reducer, initialState);

以下執行 dispatch,更新朱的票數,並觸發 state 更新。

dispatch({
type: "朱的票數",
payload: { 票數: newValue }
})

useMemo

useMemo 在於緩存數據,第二參數為一個陣列值,裡面的值影響是否重新計算。如果不透過 useMemo 做緩存,而透過方法去計算,只要畫面有任何變動( re-render )就會重新執行該方法的計算。

useCallback

保持相同 function instance 不要每次觸發Click 事件時重新re-render,第二參數為一個陣列值,其值是否重新執行其內容。

useImperativeHandle

此 API 主要是讓,父元件能夠調用到子元件的方法,如果是用 Class Component 去編寫,透過 Ref 的方式,可以取到子元件的所有方法與屬性。但在 Functional Component 需要透過 useImperativeHandle,清楚定義可使用的接口才能使用。

結果展示

補充資料
TypeScript 中使用React Hook
https://zhuanlan.zhihu.com/p/66242790

--

--

DEBUG
LT Lab
Editor for

軟體工程的可怕是沒有最優,只有更優的代碼,每天被放大鏡檢視的錯誤(BUG),已鍛鍊內心的強大,有時可能厚者臉皮裝傻帶過,更多時候是不願服輸,比昨日的自己還強大。