- Published on
React 18 alpha có gì vui?
- Authors
- Name
- Tran Tien Dat
Mới đây, React Core Team đã trân trọng thông báo The Plan for React 18, đánh dấu sự xuất hiện của React 18 alpha trên bản đồ thế giới. Lúc đầu, mình tính bay vô đọc cho vui vui thôi, mà càng đọc càng thấy vui thiệt, nên mình quyết định viết hẳn 1 bài giới thiệu luôn. Mà React có tạo hẳn một Fan Group trên Github gọi là React 18 Working Group, anh em vô thẳng đó đọc cho lẹ. Khỏi đọc blog mình viết cũng đc 😋
Nếu bạn đã đọc tới đây, thì mình xin cảm ơn rất nhiều vì đã quan tâm đến bài viết này!
Dưới đây là mấy thứ hay ho mình tìm hiểu được sau vài ngày thâm dò đến từ vị trí của React 18. Một cái note nhỏ là bản này mới là bản ALPHA thôi nên bạn cũng đừng vội đem vào sử dụng trong dự án thực tế nha. Vào test trải nghiệm chơi cho vui thì ô kê la.
Cài đặt React 18 Alpha
npm install react@alpha
npm install react-dom@alpha
Cài xong rùi thì mình cùng tìm hiểu mấy cái tính năng mới trong React 18 NHÉ! 🤟
1. New Root API
Đến với React 18, đầu tiên ta sẽ có 1 bộ Root API mới. Trong React, khái niệm "root" được hiểu là 1 cái pointer chỉ đến top-level DOM node, để React nhận biết và thực thi công việc render dưới sự quản thúc của ReactDOM.
Bình thường, nếu ta viết App chỉ với React, thì từ trên xuống dưới ta chỉ có 1 root thôi. Nhưng nếu bạn integrate React vào 1 App khác hệ thì "maybe" là bạn có thể có nhiều root tồn tại độc lập với nhau, hoặc là trong trường hợp Micro Frontends.
React mới đưa đến cho ta 2 khái niệm Root API
Legacy Root API
Những API tồn tại trong React 17 trở về trước, cách thức khai báo và cơ chế bên trong vẫn giữ nguyên như cũ. (Tuy nhiên cách này sẽ deprecated trong tương lai)
import * as ReactDOM from 'react-dom'
import App from 'App'
const container = document.getElementById('app')
// Initial render.
ReactDOM.render(<App tab="home" />, container)
// During an update, React would access
// the root of the DOM element.
ReactDOM.render(<App tab="profile" />, container)
New Root API
Chúng ta sẽ phải gọi hàm ReactDOM.createRoot() để sử dụng những tính năng mới có trong trong React 18. Theo cách này, ReactDOM sẽ expose root thành variable để user có thể sử dụng lại sau khi render hoặc update mà không cần phải pass container vào hàm render nữa.
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" />)
Read More
Discussion about New Root API
2. Automatic Batching
Batching là gì?
Batching trong React là cơ chế gộp nhiều lần update state vào chung 1 lần render nhầm tối ưu performance.
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>
)
}
Behind the scenes, React sẽ tự động group các lệnh update state setCount() và setFlag() lại, kết quả là ta sẽ chỉ thấy 1 lần render duy nhất.
Vấn đề là gì?
Từ React 17 trở về trước, việc batching chỉ được sử dụng cho các browser events (như click). Nếu bạn cần update nhiều state sau khi fetch data hoặc bên trong 1 cái callback của 1 cái Promise nào đó, thì React vẫn sẽ thực thi những lần update riêng biệt, chứ không batch chúng lại với nhau.
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>
)
}
Kết quả của đoạn snippet trên sẽ cho ra 2 lần render.
Cải tiến Batching
Đến với React 18, rất cảm ơn mấy anh em trong team React đã cập nhật batching cho cả mấy event Promise, setTimeout, và mấy cái native event handler khác luôn nhé. Nghĩa là cũng cùng 1 đoạn update như trên nhưng React sẽ chỉ render 1 lần thôi. 🥳🥳🥳
Nếu bạn không muốn batching, mà muốn các state được render độc lập thì bạn có thể sử dụng flushSync()
import { flushSync } from 'react-dom' // Note: react-dom, not react
function handleClick() {
flushSync(() => {
setCounter((c) => c + 1)
})
// React has updated the DOM by now
flushSync(() => {
setFlag((f) => !f)
})
// React has updated the DOM by now
}
Read More
Discussion about Automatic Batching
3. Concurrent Mode - startTransition API
Trên đường đời, bạn chắc hẳn đã vài lần gặp trường hợp "giật tít, đứng hình mất n giây" khi phải update / render khối lượng lớn data ngây sau khi user tương tác với web như kiểu gõ vào ô search hay scroll trang. Trong những trường hợp này, có 2 loại Updates:
Là những update trực tiếp khi user tương tác, cần được update ngây tức thì như clicking, hovering, typing, hoặc scrolling.
Là những update ít "urgent" hơn, để chuyển đổi hay hiển thị kết quả lên UI sau khi "Urgent Update" được thực thi xong.
Ví dụ:
Mình sẽ render "binary tree" theo "dept" (độ sâu) khi kéo slider.
Dễ nhận thấy việc kéo slider về số càng lớn, nghĩa là "binary tree" của mình sẽ render lại với số lượng 2^dept node được tạo ra.
Việc render node này sẽ rất nặng dẫn đến cái slider của mình bị "đứng hình" phải chờ cho cái tree render xong rồi mới kéo tiếp được.
Cứ việc nắm slider kéo qua lại là thấy thôi dễ lắm! 🤟 Nó có crash app thì f5 lại nhe mấy friend :))
Anh em developer ta có thể sử dụng 2 kỹ thuật debouncing và throttling để improve performance. Giờ thì chắc không cần nữa, vì đã có startTransition 🤭, một trong những tính năng mới trong concurrent mode ở React 18. Chỉ cần với 1 lần wrap cái startTransition vào cái "Transition Update" là xong.
<input
type="range"
min="0"
max="12"
value={inputValue}
onChange={(e) => {
// Urgent: show the input value
setInputValue(e.target.value)
// Mark any state updates inside as transitions
startTransition(() => {
// Non Urgent: show the trees
setDept(e.target.value)
})
}}
/>
Giờ đây, cái slider sẽ không phải chờ cái "tree" render xong nữa. Kéo qua kéo lại nó đã mượt hơn hẳn các bạn à :v
Read More
Discussion about startTransition API
Discussion about Real World example
Performance visualization demo 1
Performance visualization demo 2
4. New Suspense SSR Architecture
Dành cho dân chơi SSR, cho đến hiện tại cơ chế của SSR là thực hiện tuần tự theo từng bước như sau:
- Trên server, fetch data cho nguyên cái app
- Server render app thành HTML và trả về cho client.
- Client nhận HTML và load Javascript cho toàn bộ app.
- Client apply Javascript Logic cho HTML được trả về từ server. (Phản ứng hydrat hóa)
SSR giúp tăng tốc độ tương tác của user, đồng thời tăng khả năng SEO cho website. Tuy nhiên, SSR cần đầy đủ data để render HTML trả về cho client. Trong một số trường hợp, điều này trở nên bất tiện:
Ví dụ: Bạn cần render post với comment. Tuy nhiên, data của comment có thể khá nhiều nên việc fetch data comment sẽ trở nên lâu hơn, đồng nghĩa với việc trì hoãn việc load HTML page. Vì SSR phải thực hiện tuần từ các bước trên. Fetch xong data → Render HTML xong → Load JS xong → Thực hiện hydration.
React 18 với Suspense sẽ chia nhỏ app của bạn thành từng phần nhỏ hơn, mỗi phần sẽ thực hiện các bước SSR riêng biệt và độc lập.
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
Bằng việc wrap component Comment với Suspense, thay vì chờ Comments render, thì React sẽ thay bằng 1 cái Spinner.
HTML sẽ được generate ra như thế này
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width="400" src="spinner.gif" alt="Loading..." />
</section>
</main>
Khi data của comment đã load xong trên Server, React sẽ gửi thêm đoạn HTML được generate cho Comment, cùng với đó là 1 đoạn script để replace Comment vào đúng chỗ cái Spinner.
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// This implementation is slightly simplified
document.getElementById('sections-spinner').replaceChildren(document.getElementById('comments'))
</script>
Cuối cùng, ta thấy được toàn bộ component trên page được load xong.
Read More
Disussion about Suspense