React 19
2024/12/19
社内共有用。React v19 – React を少しずつ見ていく。
Actions
- 非同期 transition を使う関数のことを “Action” と呼ぶ
- ポイント
- 保留状態が管理される
- 楽観的更新がやりやすい
- エラーハンドリング簡単
- エラーが起きたら、Error Boundaries を表示したり、楽観的更新を元に戻したりできる(自動的に)
- フォームとも連携
- formのaction propに関数を渡せるようになった。この関数は “Action” になる
- この Action をベースにして、いくつかの hooks が追加されている
useActionState
const [state, wrappedAction, isPending] = useActionState(action, initialState);
- 引数に “Action” (
action
) を渡すと、実行用の ”ラップされた Action” を返す (wrappedAction
) - この “ラップされた Action” を実行すると、”Action” の最後の実行結果を返す (
state
) - また、”Action” の保留状態を返す (
isPending
)
stateは初回のレンダリングではinitialState, action が終わり次第、その戻り値に更新される
<form>
Actions
<form>
,<input>
,<button>
のaction
,formAction
prop に関数を渡せるようになった- prop に渡した関数は Action として扱われ、Action が完了すると、自動的に
<form>
がリセットされる- 手動でリセットする口もある (
requestFormReset
)
- 手動でリセットする口もある (
<form action={actionFunction}>
useFormStatus
- props なしで、Context を使わずに、親formの情報にアクセスできるhook
- 例えば、form で利用される汎用的な button コンポーネントの実装において、保留状態を取得して使う
function DesignButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending} />
}
- pending だけじゃなく、submit された data も取得できる
- 親formが無ければ null (pending の場合は false)
https://react.dev/reference/react-dom/hooks/useFormStatus
useOptimistic
- 楽観的更新が簡単にできる hook
use
- render 中にリソースを読み取るためのAPI
- promise を読み込むと、use を使っているコンポーネントは promise が resolve (fulfilled) されるまで suspend される
interface Todo {
userId: number;
id: number;
title: string;
completed: false;
}
async function fetchTodo(): Promise<Todo> {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
return res.json();
}
export function Todo() {
const todo = use(fetchTodo());
return (
<p>
{todo.title}
</p>
)
}
function App() {
return (
<Suspense fallback={"loading..."}>
<Todo />
</Suspense>
)
}
こんな感じで useEffect
無しでうまいことやれるようになるんかな、と思ったがそうではない。
👆のコードの場合、fallback が表示されっぱなしで Todo は表示されない。
ドキュメントには以下のように記述されている
use はレンダー中に作成されたプロミスをサポートしない
レンダー中に作成されたプロミスを use に渡そうとすると、React は警告を表示します。修正するには、プロミスをキャッシュできるサスペンス対応のライブラリまたはフレームワークで作られたプロミスを渡す必要があります。将来的には、レンダー中にプロミスをキャッシュしやすくする機能を提供する予定です。
「レンダ―中に作成されたプロミス」は、つまり fetchTodo()
だが、「プロミスをキャッシュ」と言われてもよくわからない。
言っていることや、そもそも fallback が表示されっぱなしになる原因を理解するには、Suspense について理解する必要がある。
そもそも <Suspense>
https://react.dev/reference/react/Suspense
Suspense による fallback と、その後のコンポーネントのレンダリングは以下の流れになる
- コンポーネントが render される過程で、Promise が throw されると、コンポーネントが「suspend」される
<Suspense>
は children(コンポーネント) が suspend すると、fallback をレンダーする- throw された Promise が resolve されると、Suspense の children を再レンダリングする
先ほどの use
に fetchTodo()
を渡していたコードでは、この流れの 3 において、再度 fetchTodo()
が実行されることになり、1~3 をループすることになってしまう(いつまで経ってもTODOは表示されない)
期待する動作( fetchTodo()
が完了したら、Todoコンポーネントに取得した Todo を表示する)を実現するには、3 (Promise が resolve され、Todoコンポーネントが再度レンダリングされるとき) において、use に渡される Promise が 1 で throw された Promise と同じである必要がある。
ドキュメントに書かれていた「プロミスをキャッシュ」とはこのことで、何らかの形で初回のPromiseをキャッシュしておいて、use に渡してあげる必要があるよ、と言うこと。
初回 (1) と同じ Promise を渡された use は resolve された Promise は throw しないので、再レンダリングされたとき、期待通りに Todo が表示される。
どうやってキャッシュする?
例えば、Tanstack Query の useQuery を使うと、キャッシュした Promise が得られる。
const { promise } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
experimental_prefetchInRender: true, // このオプションを有効にすると promise がもらえる
});