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

 Gordon Lau 劉偉中

Gordon Lau 劉偉中

2022-04-01

筆者不是經常會寫關於React的文章,對上一篇寫關於React的文章已是2019年講關於以React Hooks來編寫函數式部件(Function Component)的文章, 因為在筆者看來,React自2013年推出以來,API 已經非常穩定,近年開發重點主要放在改善效能以及改善開發者體驗(DX, Developer experience)之中。 React 17 更罕見在官網之中,際出一句No new features,殊不知這一切只是為了React 18舖路。

logo-og.png

React 18 可說是自React 16.8 推出 React Hooks 兩年多以來最大變動,其中主要變動都離不開兩個字 ⸻ 並發(Concurrency)。 也就是React 18的主要功能,都為了改善React在並發編程方程之效能支援,以及改善開發難度而設的。

Suspense

SuspenseReact 17 早已出現,但一直處於experimental,而非stableReact團隊也一早表明React 17是一個過渡性的更新,不會有 大變動。自然而言,這個大變動,在React 18 就名正言順的推出,更與現今相當流行的方法伺服器端渲染(Server Side Rendering)相容。

筆者在此舉一個例子,展示Suspense的用途,例子當然是用筆者最愛的TodoList

export function TodoList() {
    // 1. 先定義 State
    const [todos, setTodos ] = useState<TodoList>([])

    
    // 3. 用fetch去server 讀取數據
    useEffect(()=>{
        async function fetchTodos(){
            const res = await fetch('/todos')
            const todosArr = await res.json()
            setTodos(todosArr)
        }
        fetchTodos()        
    },[setTodos])


    // 2. 再定義要 render 部件的HTML
    return (
      <div>
        <h2>Todo List</h2>
        <div>
            {todos.map(todo => (
              <div key={todo.id}>
                <div>#{todo.id}</div>
                <div>{todo.title}</div>
                <div>
                  <Link to={`/todo-detail/${todo.id}`}>
                    <Button>
                      show details
                    </Button>
                  </Link>
                </div>
              </div>
            ))}
        </div>
      </div>
    )
}

這是一個典型函數式部件(Function component)的例子,有三大步驟:

  1. 先定義 Todo array 的State
  2. Render 部件的HTML
  3. 以Fetch 去讀取server的數據

由於從Server讀取數據需時而且是非同步的,在步驟23之間,有一段短時間,是只有HTML,沒有數據的。 這種方法,在React官方影片中被稱為Render-then-fetch,因為確是先render然後再fetch,這種做法,對於React 老手這種寫法當然稀鬆平常,但隨著部件的複雜程度愈高,理解就會愈來愈困難,主因在於是非同步編程(Asynchronous Programming),理解方面始終比同步編程(Synchronous Programming)較為複雜。

Suspense希望解決的問題,就是為了將這種非同步編程,變成為簡化的同步編程。 只要將 TodoList部件,包含在Suspense部件之中

// In App.tsx
export function App(){

    return (
        <Suspense fallback={<Loading />}>
            <TodoList />
        </Suspense>
    )
}

// In TodoList.tsx
export function TodoList(){
    // 這裏可以使用一個名為useFetch的custom hooks,簡化程式碼

    // 1. 從Server讀取數據,
    const { data: todos = [] } = useFetch('/todos', {
        suspense: true // can put it in 2 places. Here or in Provider
    }, []) 

    // 2. 假如數據未就緒,就顯Loading部件

    //3. Render要顯示的HTML
    return (
        <div>
            <h2>Todo List</h2>
            <div>
                {todos.map(todo => (
                <div key={todo.id}>
                    <div>#{todo.id}</div>
                    <div>{todo.title}</div>
                    <div>
                    <Link to={`/todo-detail/${todo.id}`}>
                        <Button>
                        show details
                        </Button>
                    </Link>
                    </div>
                </div>
                ))}
            </div>
        </div>
    )
}

整個結構簡單不少,因為少了useEffect的非同步編程,結果令整段程式碼更容易理解。 Suspense運作起上來,有點像try-catchloading的混合。也就是當數據未完成,就只fallback 至<Loading/>,完成後,就顯 示內容。

除了client-side rendering 以外,Suspense也能同時應用在server-side rendering 之上。 在這個討論之中,解釋了Suspense在改善SSR上的重要性。 假如我們直使用SSR,那麼Server就會等齊整個<Layout/>內的每一個部件就緒,才會將HTML送到瀏覽器。

// Client side
<Layout>
    <NavBar />
    <Sidebar />
    <RightPane>
        <Post />
        <Comments />
    </RightPane>
</Layout>

如果我們用Suspense包含如下圖的<Comments/>,在React 18 起,React 就會先將<Comments/>以外的HTML 都送到前端,而在Comments 的位置,就只會顯示一個<Spinner />以提示用戶,Comments正在載入。

// Client side
<Layout>
    <NavBar />
    <Sidebar />
    <RightPane>
        <Post />
        <Suspense fallback={<Spinner />}>
            <Comments />
        </Suspense>
    </RightPane>
</Layout>

suspense_ssr.png

Source

其他功能 : Transition

另一個React 18新加的功能,就是Transition 的 API, 作用在於將State新 分為緊急(Urgent)及非緊急(Non-urgent)兩種,Transition對應的也就是非緊急的state更新。 用戶按制、輸入文字等,必須為緊急更新(Urgent update),否則用戶很容易會覺得畫面無反應。 Transition則包含像顯示文字的State更新,這些更新縱有少少延遲,也不會影響用戶體驗。

Transition API 有一個React Hooks,名為 useTransition()

const [isPending, startTransition] = useTransition()
// isPending用來表示該Transition 是否已經完結。

startTransition(()=>{
    // 顯示Todo 內容的狀態更新不是緊急至必須馬上有反應。
    setTodos(todos);
})

其他功能: Automatic Batching

Automatic Batching則是React 為了改善效能而做的。 大家如果寫過React,都知道 狀態更新setXXX不一定是馬上執行,因為React會將多個setXXX 合成一個去處理。 也就是說以下的例子中,count會是1 不是2 。 因為React 會將這兩個更新合為一個,也就是只剩下後面的count+1

export function Counter(){
    const [count, setCount] = useState(0)
    const handleClick = ()=>{

        setCount(count + 1)
        setCount(count + 1)
        // Automatic batching
    }
    return (
        <div>
            <button onClick={handleClick}>
        </div>
    )
}

但在React 18以前,Automatic batching只會在event listener中運作, setTimeoutfetch等動作之後的狀態更新,是不會執行automatic batching的。

React 18的更新,就是將所有狀態更新都會執行automatic batching。因此以下例子在React 18中, count依然會是1,而且只會re-render一次。

export function Counter(){
    const [count, setCount] = useState(0)
    const handleClick = ()=>{
        async function callServer(){

            const res = await fetch('/count')
            const result = await res.json()
            setCount(count + 1)
            setCount(count + 1)
            // Automatic batching 
        }
    }
    return (
        <div>
            <button onClick={handleClick}>
        </div>
    )
}

嘗試階段: Server Component

React 18還有一個尚在嘗試階段的功能,就是Server Component。乍聽之下與Server Side Rendering有些相似,事實上兩者截然不同。

  • Server Side Rendering是指在Server先產生好HTML ,再到Client產生須要與用戶互動的部份
  • Server Component是指完全在Server產生HTML的做法,是React 18 的新功能,完全無須clientJavaScript

最受歡迎的React 框架Next.js,就有一個例子,專為React 18的Server Component而設。

// pages/home.server.js

import { Suspense } from 'react'

import Profile from '../components/profile.server'
import Content from '../components/content.client'

export default function Home() {
  return (
    <div>
      <h1>Welcome to React Server Components</h1>
      <Suspense fallback={'Loading...'}>
        <Profile />
      </Suspense>
      <Content />
    </div>
  )
}

Source

總結

React雖然早已在前端開發中獨佔鰲頭,領先的地位並沒有令React停止改進, React18確實使前端開發者又進一步,在效能上及開發難度上都改善不少, 實在是筆者這些恆常React開發者的福音啊。

留言

延伸閱讀

React Hooks(四):全函數式React

React Hooks(三):Redux-React-Hook

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

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

到底React Hooks有何特別(二)?淺談useEffect及useReducer

到底React Hooks 有何特別?