Skip to Content

React 18 新功能能用篇: Flappy Cloud Game

React 18 新功能能用篇: Flappy Cloud Game
Andrew Shek
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

Flappy Cloud Demo Code

Comments

Read More

React Hooks(三):Redux-React-Hook

React Hooks(三):Redux-React-Hook

React Hooks(三):Redux-React-Hook
Gordon Lau
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

React Hooks(四):全函數式React

React Hooks(四):全函數式React
Gordon Lau
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

Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 2

Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 2
Andrew Shek
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 登場 ! 新增功能大簡介

React 18 登場 ! 新增功能大簡介

React 18 登場 ! 新增功能大簡介
Gordon Lau
2022-04-01

`React` 18 可說是自`React` 16.8 推出 `React Hooks ` 兩年多以來最大變動,其中主要變動都離不開兩個字 ⸻ 並發(Concurrency)。


Privacy PolicySite Map

© 2022 Tecky Academy Limited

hello@tecky.iot.me/TeckyAcademy
+852 9725 6400+44 07570 549084+1 (647) 556-3084
Caring Company 2019-2022
TQUK Approved Centre
aws_partner
Reimagine Education Challenge Award