What’s coming in React 18
在計畫中有提到了三個主要新功能
- automatic batching
- startTransition
- new streaming ssr with React.lazy
並且計劃在讓舊版本在最小的改動之下,就能使用這些提升而不用重寫整個專案。
Automatic Batching
what is batching?
React 18 會在核心中新增這項改進,一般用戶甚至不會意識到有這變化,不過還是來看看什麼是 batching
。
batching
是 React 把多個狀態更新濃縮成單個,來達到更好的效能,以底下這段 code 來說,每次點擊會觸發兩次 setState,然後 react 會把這兩次濃縮成一次更新,只會觸發一次 re-render。
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // Does not re-render yet
setFlag(f => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
這是個很棒的功能,可以減少很多不必要的 re-render ,也不會再出現那種狀態更新到一半的情況。但是,現階段的 React 並沒有辦法處理全部的更新,目前的 React 只能將事件裏的更新 batch 起來,像是底下這種在 fetch
之後的 setState ,就無法處理了。
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 17 and earlier does NOT batch these because
// they run *after* the event in a callback, not *during* it
setCount(c => c + 1); // Causes a re-render
setFlag(f => !f); // Causes a re-render
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
在 React 18 前,只有 event handler 裡面的更新會被 batch, promise
、 setTimeout
、 native event
等等都是不會被 batch 的。
What is automatic batching
在 React 18 中使用 createRoot
,所有的更新就都會 batch 了。
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// Create a root.
const root = ReactDOM.createRoot(container);
// Initial render: Render an element to the root.
root.render(<App tab="home" />);
// During an update, there's no need to pass the container again.
root.render(<App tab="profile" />);
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 18 and later DOES batch these:
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
在 React 18 中 fetch
、 promise
、 setTimeout
等等的 setState 行為就一樣了。
startTransition
在大型專案裡,如果做個 filter 來 filter 一個很大的 table ,有時候會導致畫面的卡頓,舉例來說你可能會寫成這樣:
// Urgent: Show what was typed
setInputValue(input); // light
// Not urgent: Show the results
setSearchQuery(input); // heavy
其實使用者比較希望馬上看到 inputValue 的變化,這樣的互動感覺是好的,而 table 的變化可以稍微的慢一點沒關係,但原本的寫法還是得等到整個結果完成才 render。
現在有新的 api startTransition
可以使用
import { startTransition } from 'react';
// Urgent: Show what was typed
setInputValue(input);
// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results
setSearchQuery(input);
});
在 React 中更新有兩種策略
- Urgent updates: reflect direct interaction, like typing, clicking, pressing, and so on.
- Transition updates: transition the UI from one view to another.
在更新時只要告訴 React 那些更新是可以設定為 Transition updates
那麼這些更新就會是可以被 interrupt 的。
Suspens SSR Architecture
在 React 中 SSR 通常有以下幾個步驟:
- Server 端,fetch 整個 app 的 data
- 接著 server render 出整個 app 要用的 HTML 並且回傳
- 接著 client 接收整包的 javascript code
- 接著 client 將 javascript 與 server-generated HTML 連結 (hydration)
關鍵的是每個步驟都是要處理整個 app 的 fetch 與 render,導致當app太大時反而很慢。
在 React 18 中可以用 <Suspense>
來將整個 app break down 成較小的獨立單位,來讓 SSR可以分開進行。
Streaming HTML and Selective Hydration
原本的 SSR
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section>
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</section>
</main>
client 端會先收到
接著 load javascript 並且 hydrate
React 18 中的 SSR
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
將 <Comments />
放到 <Suspense/>
中, 就是告訴 React,不用等 <Comments />
裡的東西。
另外在 client 端也可以加入 lazy
來讓前端的 code 是分段載入的,而不是一大包。
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
心得
以上就是 React 18 計畫裡提到的三個新功能,看起來都是加入了 fiber 以後,讓 react render 變得更有彈性,所以在做功能時可能要想到耦合度不要太高,能夠功能區塊切割完整的話,未來才能享用這些新功能。