跳至主要内容

React 19 RC更新筆記

2024的4月25號,React官方在Blog上發布了最新的訊息Link,關於React 19RC能在npm上使用下載了。這裡簡易做個紀錄,看一下新的React 19添加了哪些的特性。

這裡先列出幾個重要的特性: ActionuseRSC等等

Action

我們過去常做的發送請求,並根據狀態來更新我們的App,並且要自己手動去處理這些狀態,像是等待狀態(pending status)、錯誤處理(error handling)、依回應請求來更新等等。

而在React 19,提供了async function來自動處理pending state、error、forms和optimistic update automatically。

像是現在可以使用useTransition來處理pending state,React提供的例子如下:

// Using pending state from Actions
function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();

const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect("/path");
})
};

return (
<div>
<input value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}

我們能注意到使用useTransition來等待過渡(transition)的過程,如果是以前的話要自己添加一個pending狀態來去處理:

// previous
const [isPending, setIsPending] = useState(false);

const handleSubmit = async () => {
setIsPending(true);
const error = await updateName(name);
setIsPending(false);
if (error) {
setError(error);
return;
}
redirect("/path");
};

然後React官方將function在使用非同步過渡這個行為稱為"Action"

為何使用Action?

action會自動幫我們在submit資料時進行處理,像是:

  • 等待回傳狀態
  • 進行錯誤處理
  • 處理<form>表單(在之前的話通常會使用react創建state來管理,或者是使用react-hook-form去處理)
  • 進行樂觀更新(optimistic updates),樂觀更新的概念是app假設server回傳的響應是成功的,而不用等待後端回傳並確認是否已達成,當更新完成或出錯時,會切換回正確的值

useActionState(取代之前的useFormState)

const [state, formAction] = useActionState(fn, initialState, permalink?);

在先前的版本是ReactDOM.useFormState,現在被廢棄並改成使用React.useActionState。並且使用方式如下:

// Using <form> Actions and useActionState
function ChangeName({ name, setName }) {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get("name"));
if (error) {
return error;
}
redirect("/path");
return null;
},
null,
);

return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}

並且現在ReactDOM,在form上提供了action props來傳入,可以<form action={actionFunction}>,並且在action成功執行後,react會自動重置該uncontrolled元件。

以往我們要控制該元件時,會將元件改成使用controlled component或者是使用react-hook-form套件。

useFormStatus

react提供了該hook讓我們能在子元件獲取最近的form上下文狀態,使用該hooks時會向父元件找尋直到遇到form。

import {useFormStatus} from 'react-dom';

function DesignButton() {
const {pending} = useFormStatus();
return <button type="submit" disabled={pending} />
}

useOptimistic

先前提過,該hook會優先假設server回傳的響應是成功的。使用方式如下:

function ChangeName({currentName, onUpdateName}) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);

const submitAction = async formData => {
const newName = formData.get("name");
setOptimisticName(newName);
const updatedName = await updateName(newName);
onUpdateName(updatedName);
};

return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<p>
<label>Change Name:</label>
<input
type="text"
name="name"
disabled={currentName !== optimisticName}
/>
</p>
</form>
);
}

use

該hook可以讓我們去讀取類似於Promise或context的resource value,並且與一般hook不同的是use可以在條件或循環語句下使用。

function Hi({ show }) {
if (show) {
const theme = use(ThemeContext);
return <hr className={theme} />;
}
return false;
}

使用方式如下:

case 1: Promise

import {use} from 'react';

function Comments({commentsPromise}) {
// `use` will suspend until the promise resolves.
const comments = use(commentsPromise);
return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({commentsPromise}) {
// When `use` suspends in Comments,
// this Suspense boundary will be shown.
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}

當Promise調用use,會與Error Boundary和Suspend相互作用。也就是傳遞給use的Promise在pending時,調用use的component也會被掛起suspend,並且如果再Suspend下,就會顯示fallback。

case 2: Context

import {use} from 'react';
import ThemeContext from './ThemeContext'


function MyPage() {
return (
<ThemeContext.Provider value="dark">
<Heading />
</ThemeContext.Provider>
);
}
// ...

function Heading({children}) {
if (children == null) {
return null;
}

// This would not work with useContext
// because of the early return.
const theme = use(ThemeContext);
return (
<h1 style={{color: theme.color}}>
{children}
</h1>
);
}

這邊use會返回傳遞的context value,React會搜索並找尋最接近的context provider來返回值,其實就跟useContext一樣。

React Server Component

不同於SSR,能在bundling前進行提前渲染,在sever的單獨環境去創建元件。RSC可以在建置時在CI伺服器上執行一次,也可以使用Web伺服器針對每個請求執行。

Server Components without a Server:

該模式使用者需要下載並解析額外的 75K(gzip 壓縮)庫,並在頁面加載後等待第二個請求來獲取數據,只是為了呈現在頁面生命週期內不會更改的靜態內容。如果使用Server component。我們能在build time時render元件一次

// With RSC
import marked from 'marked'; // Not included in bundle
import sanitizeHtml from 'sanitize-html'; // Not included in bundle

async function Page({page}) {
// NOTE: loads *during* render, when the app is built.
const content = await file.readFile(`${page}.md`);

return <div>{sanitizeHtml(marked(content))}</div>;
}

// Without RSC
// bundle.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function Page({page}) {
const [content, setContent] = useState('');
// NOTE: loads *after* first page render.
useEffect(() => {
fetch(`/api/content/${page}`).then((data) => {
setContent(data.content);
});
}, [page]);

return <div>{sanitizeHtml(marked(content))}</div>;
}

// api.js
app.get(`/api/content/:page`, async (req, res) => {
const page = req.params.page;
const content = await file.readFile(`${page}.md`);
res.send({content});
});

然後,渲染的輸出可以在伺服器端渲染 (SSR) 為 HTML 並上傳到 CDN。當應用程式載入時,用戶端將看不到原始的 Page 元件,也不會看到用於渲染 markdown 的昂貴的lib,客戶端只會看到渲染的輸出。也就是內容在第一次page load是可見的,並且bundle不包含渲染static render所需的昂貴的lib。

<div><!-- html for markdown --></div>

Server Components with a Server:

Server component也可以在request頁面期間在 Web 伺服器上運行,讓我們不用建立API即可存取data layer。它們在app bundle前呈現,並且可以將資料和 JSX 作為 props 傳遞給客戶端元件。

// With RSC
import db from './database';

async function Note({id}) {
// NOTE: loads *during* render.
const note = await db.notes.get(id);
return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}

async function Author({id}) {
// NOTE: loads *after* Note,
// but is fast if data is co-located.
const author = await db.authors.get(id);
return <span>By: {author.name}</span>;
}

// Without RSC
// bundle.js
function Note({id}) {
const [note, setNote] = useState('');
// NOTE: loads *after* first render.
useEffect(() => {
fetch(`/api/notes/${id}`).then(data => {
setNote(data.note);
});
}, [id]);

return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}

function Author({id}) {
const [author, setAuthor] = useState('');
// NOTE: loads *after* Note renders.
// Causing an expensive client-server waterfall.
useEffect(() => {
fetch(`/api/authors/${id}`).then(data => {
setAuthor(data.author);
});
}, [id]);

return <span>By: {author.name}</span>;
}

// api
import db from './database';

app.get(`/api/notes/:id`, async (req, res) => {
const note = await db.notes.get(id);
res.send({note});
});

app.get(`/api/authors/:id`, async (req, res) => {
const author = await db.authors.get(id);
res.send({author});
});

End

剩下還有很多改進的部分,像是ref as prop,hydration改進等等,這裡簡易的挑了幾個地方做個紀錄,盛譽的部分,如果感興趣的話,能到React Blog去觀看,並且下個月有React conf,可能會在該會議上公布和發表更多的新特性,這裡先記錄到這邊。

Reference

React Docs React Blog