React 19 RC更新筆記

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


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