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的做法,是React
18 的新功能,完全無須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
停止改進,
React
18確實使前端開發者又進一步,在效能上及開發難度上都改善不少,
實在是筆者這些恆常React
開發者的福音啊。