Skip to main content

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

· 9 min read
戈頓
Developer & Co-founder of Tecky Academy

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

React Logo

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

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開發者的福音啊。