React 18 新功能能用篇: Flappy Cloud Game
2022-05-15
React 18 正式發佈
最近React 18 正式版終於發佈。因為今次新版本有重要既更新,所以坊間有好多blog講解React 18更新左既功能。因為我地都出過一篇blog講解React 18新功能(blog傳送門),所以今次筆者唔再重複講解。今次是想示範如何應用React 18新功能入React Project當中,所以筆者示範是寫Flappy Cloud過程中點運用以下React 18既新功能:
- Automatic Batching
- startTransition
- Hook : useTransition
- Hook : useId
- Hook : useDeferredValue
Flappy Cloud 功能
- 遊戲玩法:按鍵不停令雲仔飄浮,同時避開水管。
- 按鍵是需要估的。可以從Word List既字(估出被_的字母)/相信"山埃Tips"既提示按鍵。
- 大家有興趣體驗下,可以入去試下(Flappy Cloud)。
- 有用家反映太難玩,我已經改左按任何鍵都可以令雲仔飄浮。
建立Project
-
打以下Command建立Project
npx create-react-app flappy-cloud --template redux-typescript
-
打開index.tsx, 改用createRoot,因為Application要使用Concurrent Mode。
const root = createRoot(container);
步驟一 : 建立 React Components
- 用CSS建立大海
- CSS : 建立綠色水管
- React Component: 移動吧!綠色水管
- CSS : 建立白雲
- React Component: 讓白雲自由走動
- React Component: 控制白雲上落。按任何鍵都可以令白雲上升。
- React Component: 戴入字典(超過2萬個字同解釋),然後隱藏每個生字中的某幾個字母。只要估中被隱藏字母可以令白雲上升。注意:此動作每次keyboard有輸入都要做一次。相當大loading(我故意做大loading,下一節講解原因。)。
出事了!隻Game點解玩唔到?
出事位#1 : 計次數、控制白雲"卡"死了。
原因是React 18開始,React開始提示大家轉用createRoot。轉用createRoot之後,「Status更新」就會轉成Concurrent Mode。
//index.tsx const container = document.getElementById('root')!; const root = createRoot(container); root.render( <Provider store={store}> <App /> </Provider> );
Concurrent Mode,簡單講,所有「Status更新」是一齊進行。如果遇到大loading動作,例如,戴入字典,所以電腦工作需時,影響到其他「Status更新」既回應,例如,更新input text box(react change 事件)。使用介面(User Interface)出現停頓情況,使用者體驗(User Experience)就會差。
const keyPressHandler = (event: any) => { const input_key = event.target.value[event.target.value.length - 1]; //Automatic Batching(setCommand + setAction) setCommand(input_key.toUpperCase()); //令AutoScrollingList 重新rendering,即是重新戴入字典😨。 setAction({ loop: action.loop + 1, key: input_key }); }
如果要解決問題,我地要幫React做分流。我地需要識別那一個「Status更新」是「緊急」,那一個「非緊急」。「緊急工作」會優先完成。即是。。。如果「非緊急工作」進行中,「緊急工作」突然亂入。「非緊急工作」會暫停,讓「緊急工作」完成後繼續「非緊急工作」。React預設所有「Status更新」是「緊急」,所以等我地需要用startTransition Function包含指明的「非緊急工作」。
使用startTransition後, 回復正常!
const keyPressHandler = (event: any) => { const input_key = event.target.value[event.target.value.length - 1]; /*React 18 是行Concurrent,所以React 會做分流*/ startTransition(() => { //次緊急 setAction({ loop: action.loop + 1, key: input_key }); }); //緊急 setCommand(input_key.toUpperCase()); }
1.如何識別"緊急"/"非緊急"?
緊急:用家行為回應,例如,打字、點擊、拖動等。不即時回應會使使用介面出現停頓情況,使用者體驗(User Experience)就會差。
非緊急:不影響系統運作/用家體驗的動作,例如,移動綠色水管,戴入字典。
2.題外話
舊React版本下,除Callback外,如果有10個status更新,就行10次rendering,如此類推。Callback情況下,無論有幾多個status更新都可以用一次rendering完成。
新版本下,任何情況,例如,Promise、setTimeout、React事件等,無論有幾多個status更新都可以用一次rendering完成,稱為"Automatic Batching"。
出事位#2 : "按鍵提示清單"Component變成次緊急後,有短時間白畫面。
startTransition 只是做分流,無顯示任何進度提示。如果想做,就需要用到useTransition Hook。
import React, { startTransition,useEffect,useState } from 'react' const AutoScrollingList = ({ action,checking }: { action: {loop:number,key?:string},checking:(match:boolean,answer:string)=>void }) => { useEffect(() => { const wordArray = Object.keys(dictionary); startTransition(() => { const newWordList = wordArray.slice(action.loop, action.loop + 15000); const newAnswer: string[] = answer.slice(0); const idx = newAnswer.findIndex(x => x.toLowerCase() === action.key?.toLowerCase()); if (idx >= 0){ newAnswer.splice(idx, 1); } const select = Math.floor(Math.random()*newAnswer.length); checking(idx>=0,newAnswer[select]); if (newAnswer.length === 0) { let i = 0; while (i < 5) { const idx = Math.random() * 26; const key = 64 + idx; if (newAnswer.includes(String.fromCharCode(key))) { continue; } newAnswer.push(String.fromCharCode(key)); i++; } setAnswer(newAnswer); } const words: string[] = newWordList.map(word => { let w = word; for(let char of newAnswer){ w=w.replaceAll(char.toLowerCase(), "_") } return w; }); setItems(words); }); }, [action.loop]); });
使用useTransition Hook就可以出進度提示。
import React, { useEffect, useRef, useState, useTransition } from 'react' //... const AutoScrollingList = ({ action,checking }: { action: {loop:number,key?:string},checking:(match:boolean,answer:string)=>void }) => { const [isPending, startTransition] = useTransition(); //useEffect(...) //no change return ( <div className={styles.container}> {isPending ? " Loading..." : null /*show progress here if isPending is true*/} <div className={styles.scrollList}> {items && items.map((item, index) => <div className={styles.element} key={useId()}>{item}</div>)} <div ref={bottomRef} className="list-bottom"></div> </div> </div> ) });
1.題外話
React 18有一個hook叫useId可以generate id。用"大海"Component中的入面兩個"浪"示範。
export default function App() { const wave1 = useId(); //<--added const wave2 = useId(); //<--added return ( <div style={{ display: "flex" }}> {/*....*/} <Wave key={wave1/*changed*/} bottom={"-200px"} color={"darkblue"} offset={"-20px"} delay={"2s"} /> <Wave key={wave2/*changed*/} bottom={"-200px"} color={"blue"} offset={"20px"} delay={"0s"} /> </div> ); }
出事位#3 : 顯示次數仍然不太順
顯示次數都不需要實時同步,可以延遲顯示最新次數(寧可延遲,但是唔好跳過個別數字💢)。React 18有一個hook叫useDeferredValue可幫到手。
原本的Code。。。。
const [count, setCount] = useState(0); return ( {/*....*/} <div className={styles.controlPanel}> <div className={styles.count}>{count}</div> <div>估中按鍵次數</div> <input style={style} className={styles.keyInput} type="text" value={command} onChange={keyPressHandler} autoFocus /> <div>你輸入的按鍵</div> <div className={styles.count}>{tip}</div> <div>山埃Tips(信不信由你!)</div> <div className={styles.manual}> <h3>玩法</h3> <Manual /> </div> </div> )
如果使用useDeferredValue就變成....
const [count, setCount] = useState(0); const deferredCount = useDeferredValue(count);//<--added return ( {/*....*/} <div className={styles.controlPanel}> <div className={styles.count}>{deferredCount/*changed*/}</div> <div>估中按鍵次數</div> <input style={style} className={styles.keyInput} type="text" value={command} onChange={keyPressHandler} autoFocus /> <div>你輸入的按鍵</div> <div className={styles.count}>{tip}</div> <div>山埃Tips(信不信由你!)</div> <div className={styles.manual}> <h3>玩法</h3> <Manual /> </div> </div> )
總結
我地日常開發React Project未必感受到兩者(使用舊React vs 使用React 18 Features)在表現(Performance)上有明顯分別。因為電腦硬件比較好(足以應付loading比較大的工作),或者項目性質簡單(無大loading工作),所以兩者表現上無明顯分別。但是。。。。
如果用家既電腦硬件比較「入門」,或者,隨著Project功能越来越多,表現差會慢慢浮現出嚟。
所以,是React Project開發過程必須考慮到如何提升表現,目的令所有用家得到"好"同"相同"的使用體驗。透過運用React 18新功能提升表現外,還可運用其他React原生功能提升表現,例如,Memo,useMemo,useCallback等。。。
大家可以試下是自己的React Project試下用React 18同其他工具提升表現。Enjoy~
Source Code
留言
閱讀更多
React Hooks(三):Redux-React-Hook
2019-03-27
React Hooks在React 16.8.0的版本正式成為React的正式功能。正如前兩篇所言,React Hooks簡化了寫複雜代碼的難度,亦令React的函數式部件(Functional Components)亦能使用state及props,可是傳說中React Hooks將會取代Redux呢?卻一直都是只聞樓梯響。這篇文章就會介紹一個筆者認為頗有前景的組件,就是在Github中的facebookincubator中的redux-react-hook。
React Hooks(四):全函數式React
2019-09-09
筆者在上年十一月React Hooks剛發佈時,就寫過關於React Hooks的應用,如何簡化開發React 應用時要寫的程式碼,之後又介紹了Redux-React-Hooks這個筆者認為有不錯前途的組件,雖然隨著React-Redux加入了React Hooks的應用,現在寫React + Redux應用,已無需再寫長長的mapStateToProps及mapDispatchToProps。
Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 2
2022-02-22
上集已經詳細講解如何寫Query/Mutation Statement及如何是Apollo GraphQL提供的Playground做測試。下一步就是請解如何是Client Side Application(以React Application為例)中使用Query/Mutation Statement。我會分開兩個版本示範,實作GraphQL Client (基本版)及GraphQL Client (應用版 - Apollo Client)。
React 18 登場 ! 新增功能大簡介
2022-04-01
`React` 18 可說是自`React` 16.8 推出 `React Hooks ` 兩年多以來最大變動,其中主要變動都離不開兩個字 ⸻ 並發(Concurrency)。