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

React 18 可說是自React 16.8 推出 React Hooks 兩年多以來最大變動,其中主要變動都離不開兩個字 ⸻ 並發(Concurrency)。
也就是React 18的主要功能,都為了改善React在並發編程方程之效能支援,以及改善開發難度而設的。
Suspense
Suspense在 React 17 早已出現,但一直處於experimental,而非stable。React團隊也一早表明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)的例子,有三大步驟:
- 先定義 Todo array 的State
- Render 部件的HTML
- 以Fetch 去讀取server的數據
由於從Server讀取數據需時而且是非同步的,在步驟2與3之間,有一段短時間,是只有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-catch與loading的混合。也就是當數據未完成,就只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>

其他功能 : 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中運作,
setTimeout,fetch等動作之後的狀態更新,是不會執行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的做法,是React18 的新功能,完全無須client的JavaScript
最受歡迎的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>
);
}
總結
React雖然早已在前端開發中獨佔鰲頭,領先的地位並沒有令React停止改進,
React18確實使前端開發者又進一步,在效能上及開發難度上都改善不少,
實在是筆者這些恆常React開發者的福音啊。
