Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ServerAction で Progressive Enhancement はどこまで頑張れ...

ServerAction で Progressive Enhancement はどこまで頑張れるか? / progressive-enhancement-with-server-action

Node学園 42時限目 Next.js AppRouterについて https://nodejs.connpass.com/event/315443/

Takepepe

April 30, 2024
Tweet

More Decks by Takepepe

Other Decks in Programming

Transcript

  1. 実践Next.js – App Router で進化する Web アプリ開発 ▪ 2024.3/16 技術評論社より刊行 ▪ Next.js

    App Router を題材にした書籍 ▪ 一通りの機能を備えたサンプル App を対象に解説 ▪ 公式ドキュメントの分かりづらい箇所を重点的に
  2. React や Next.js ドキュメントでも使用されているワード ▪ Progressive Enhancement とは? ・ MDN引用   https://developer.mozilla.org/ja/docs/Glossary/Progressive_Enhancement   >

    可能な限り多くのユーザーに不可欠なコンテンツと   機能のベースラインを提供することを中心とした設計哲学 Progressive Enhancement と React 【1】Progressive Enhancement に関する書籍内容の紹介
  3. ▪ Server Component/Server Action で身近になった設計指針 ・ useFormState と Server Action により、JS オフでも

    POST が可能に Progressive Enhancement と React 【1】Progressive Enhancement に関する書籍内容の紹介
  4. ▪ Server Component/Server Action で身近になった設計指針 ・ useFormState と Server Action により、JS オフでも

    POST が可能に ・ ハイドレーション前でも、Form の送信が可能 Progressive Enhancement と React 【1】Progressive Enhancement に関する書籍内容の紹介
  5. API Client & API Routes による従来アプローチでは不可能だった実装 ▪ Server Component/Server Action で身近になった設計指針

    ・ useFormState と Server Action により、JS オフでも POST が可能に ・ ハイドレーション前でも、Form の送信が可能 Progressive Enhancement と React 【1】Progressive Enhancement に関する書籍内容の紹介
  6. Form 機能のベースラインとして、 JS オフでも最低限 POST ができるように ▪ Server Component/Server Action で身近になった設計指針

    ・ useFormState と Server Action により、JS オフでも POST が可能に ・ ハイドレーション前でも、Form の送信が可能 Progressive Enhancement と React 【1】Progressive Enhancement に関する書籍内容の紹介
  7. const [formState, formDispatch] = useFormState(updateAction, initialFormState); ▪ useFormState フックを使用する ・ Client Component

    で使用するフック JS オフでも機能する Form 実装 【1】Progressive Enhancement に関する書籍内容の紹介
  8. const [formState, formDispatch] = useFormState(updateAction, initialFormState); ▪ useFormState フックを使用する ・ Client Component

    で使用するフック ・ 第 1 引数には Server Action を渡す(updateAction) JS オフでも機能する Form 実装 【1】Progressive Enhancement に関する書籍内容の紹介
  9. const [formState, formDispatch] = useFormState(updateAction, initialFormState); ▪ useFormState フックを使用する ・ Client Component

    で使用するフック ・ 第 1 引数には Server Action を渡す(updateAction) ・ 第 2 引数には「状態(initialFormState)」を渡す JS オフでも機能する Form 実装 【1】Progressive Enhancement に関する書籍内容の紹介
  10. const [formState, formDispatch] = useFormState(updateAction, initialFormState); ▪ useFormState フックを使用する ・ Client Component

    で使用するフック ・ 第 1 引数には Server Action を渡す(updateAction) ・ 第 2 引数には「状態(initialFormState)」を渡す ・ Submit を跨いで更新される formState を使う JS オフでも機能する Form 実装 【1】Progressive Enhancement に関する書籍内容の紹介
  11. const [formState, formDispatch] = useFormState(updateAction, initialFormState); ▪ useFormState フックを使用する ・ Client Component

    で使用するフック ・ 第 1 引数には Server Action を渡す(updateAction) ・ 第 2 引数には「状態(initialFormState)」を渡す ・ Submit を跨いで更新される formState を使う ・ <form> の action 属性には formDispatch を使う JS オフでも機能する Form 実装 【1】Progressive Enhancement に関する書籍内容の紹介
  12. const [formState, formDispatch] = useFormState(updateAction, initialFormState); ▪ useFormState フックを使用する ・ Client Component

    で使用するフック ・ 第 1 引数には Server Action を渡す(updateAction) ・ 第 2 引数には「状態(initialFormState)」を渡す ・ Submit を跨いで更新される formState を使う ・ <form> の action 属性には formDispatch を使う JS オフでも機能する Form 実装 【1】Progressive Enhancement に関する書籍内容の紹介
  13. export async function updateAction( prevState: FormState, formData: FormData ): Promise<FormState>

    {} useFormState に渡す ServerAction 【1】Progressive Enhancement に関する書籍内容の紹介 useFormState に渡す Server Action は、実装制約を満たす必要がある
  14. export async function updateAction( prevState: FormState, formData: FormData ): Promise<FormState>

    {} useFormState に渡す ServerAction 【1】Progressive Enhancement に関する書籍内容の紹介 第一引数に FormState をとり、FormState を返す非同期関数とすること
  15. export type FormState = { title: string; description: string; updatedAt:

    string; error: Error | null; }; type Error = { message: string; status: number; fieldErrors?: Record<string, { message: string }>; }; useFormState に渡す State(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 Error が発生したか否かを判定する状態をもつ
  16. export type FormState = { title: string; description: string; updatedAt:

    string; error: Error | null; }; type Error = { message: string; status: number; fieldErrors?: Record<string, { message: string }>; }; useFormState に渡す State(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 Error が発生したか否かを判定する状態をもつ
  17. export async function updateAction(prevState: FormState, formData: FormData): Promise<FormState> { try

    { // ★: バリデーションエラーが発生した場合 catch 句へ const payload = validateFormData(formData); // ...省略 } catch (err) { // ★: Zod のバリデーションエラーをマッピング if (err instanceof ZodError) { return handleError(prevState, { ...errors[400], fieldErrors: transformFiledErrors(err), }); } return handleError(prevState, errors[500]); } } ServerAction 内で発生したエラー表現(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 例えば、バリデーションエラーが発生した場合
  18. export async function updateAction(prevState: FormState, formData: FormData): Promise<FormState> { try

    { // ★: バリデーションエラーが発生した場合 catch 句へ const payload = validateFormData(formData); // ...省略 } catch (err) { // ★: Zod のバリデーションエラーをマッピング if (err instanceof ZodError) { return handleError(prevState, { ...errors[400], fieldErrors: transformFiledErrors(err), }); } return handleError(prevState, errors[500]); } } ServerAction 内で発生したエラー表現(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 例えば、バリデーションエラーが発生した場合
  19. export async function updateAction(prevState: FormState, formData: FormData): Promise<FormState> { try

    { // ★: バリデーションエラーが発生した場合 catch 句へ const payload = validateFormData(formData); // ...省略 } catch (err) { // ★: Zod のバリデーションエラーをマッピング if (err instanceof ZodError) { return handleError(prevState, { ...errors[400], fieldErrors: transformFiledErrors(err), }); } return handleError(prevState, errors[500]); } } ServerAction 内で発生したエラー表現(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 バリデーションエラーが throw される
  20. export async function updateAction(prevState: FormState, formData: FormData): Promise<FormState> { try

    { // ★: バリデーションエラーが発生した場合 catch 句へ const payload = validateFormData(formData); // ...省略 } catch (err) { // ★: Zod のバリデーションエラーをマッピング if (err instanceof ZodError) { return handleError(prevState, { ...errors[400], fieldErrors: transformFiledErrors(err), }); } return handleError(prevState, errors[500]); } } ServerAction 内で発生したエラー表現(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 handleError 関数でエラー発生状態の FormState を返す
  21. export async function updateAction(prevState: FormState, formData: FormData): Promise<FormState> { try

    { // ★: バリデーションエラーが発生した場合 catch 句へ const payload = validateFormData(formData); // ...省略 } catch (err) { // ★: Zod のバリデーションエラーをマッピング if (err instanceof ZodError) { return handleError(prevState, { ...errors[400], fieldErrors: transformFiledErrors(err), }); } return handleError(prevState, errors[500]); } } ServerAction 内で発生したエラー表現(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 FormState に Error が含まれていたら「エラー文言」を表示する
  22. <form action={formDispatch} onSubmit={handleSubmit}></form> function handleSubmit(event: FormEvent<HTMLFormElement>) { try { const

    formData = new FormData(event.currentTarget); // バリデーションエラーが発生した場合 catch 句へ validateFormData(formData); // ...省略(何もしない) } catch (err) { //★: Form のサブミット(action 実行)を中止 event.preventDefault(); // ...省略 } } onSubmit による事前バリデーション(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 JS オン環境では action の前に onSubmit が実行される
  23. <form action={formDispatch} onSubmit={handleSubmit}></form> function handleSubmit(event: FormEvent<HTMLFormElement>) { try { const

    formData = new FormData(event.currentTarget); // バリデーションエラーが発生した場合 catch 句へ validateFormData(formData); // ...省略(何もしない) } catch (err) { //★: Form のサブミット(action 実行)を中止 event.preventDefault(); // ...省略 } } onSubmit による事前バリデーション(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 JS オン環境では action の前にバリデーションを実行する
  24. <form action={formDispatch} onSubmit={handleSubmit}></form> function handleSubmit(event: FormEvent<HTMLFormElement>) { try { const

    formData = new FormData(event.currentTarget); // バリデーションエラーが発生した場合 catch 句へ validateFormData(formData); // ...省略(何もしない) } catch (err) { //★: Form のサブミット(action 実行)を中止 event.preventDefault(); // ...省略 } } onSubmit による事前バリデーション(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 JS オン環境では、不正な入力値の場合「 Submit を中止する」
  25. <form action={formDispatch} onSubmit={handleSubmit}></form> function handleSubmit(event: FormEvent<HTMLFormElement>) { try { const

    formData = new FormData(event.currentTarget); // バリデーションエラーが発生した場合 catch 句へ validateFormData(formData); // ...省略(何もしない) } catch (err) { //★: Form のサブミット(action 実行)を中止 event.preventDefault(); // ...省略 } } onSubmit による事前バリデーション(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 JS オン環境では、不正な入力値の場合「 Submit を中止する」
  26. <form action={formDispatch} onSubmit={handleSubmit}></form> function handleSubmit(event: FormEvent<HTMLFormElement>) { try { const

    formData = new FormData(event.currentTarget); // バリデーションエラーが発生した場合 catch 句へ validateFormData(formData); // ...省略(何もしない) } catch (err) { //★: Form のサブミット(action 実行)を中止 event.preventDefault(); // ...省略 } } onSubmit による事前バリデーション(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介 正常入力の場合、Submit は中止しない -> action が実行される
  27. ▪ 事前バリデーションは「 より良い体験の一部」という方針 ・ JS オフ時でも、Form の送信はできる ・ JS オフ時でも、Server Action 内でバリデーションはする ・ JS

    オン時には、追加で事前バリデーションをする onSubmit による事前バリデーション(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介
  28. 「無駄に Server Action が送られないなら、より嬉しいよね」という方針 ▪ 事前バリデーションは「 より良い体験の一部」という方針 ・ JS オフ時でも、Form の送信はできる ・ JS

    オフ時でも、Server Action 内でバリデーションはする ・ JS オン時には、追加で事前バリデーションをする onSubmit による事前バリデーション(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介
  29. 詳細は「実践 Next.js」をご覧ください ▪ 事前バリデーションは「 より良い体験の一部」という方針 ・ JS オフ時でも、Form の送信はできる ・ JS オフ時でも、Server Action

    内でバリデーションはする ・ JS オン時には、追加で事前バリデーションをする onSubmit による事前バリデーション(書籍例) 【1】Progressive Enhancement に関する書籍内容の紹介
  30. Pros / Cons を検討のうえ判断が必要 ▪ JS オフ環境でも使用できる(動機づけとして弱い) ▪ ハイドレーションに関係なく即座に Form 送信ができる ・ ロースペックデバイスに効果あり?

    ・ 重厚なページで効果あり? ・ INP(Core Web Vitals)に効果あり? Progressive Enhancement を維持する目的は? 【2】Progressive Enhancement 対応はどこまでやるべきか?
  31. ▪ useActionState とは? ・ Renames useFormState to useActionState ・ Adds a pending state

    to the returned tuple 【A】useActionState が生える 【3】Server Action に関する近況アップデート https://github.com/facebook/react/pull/28491
  32. ▪ useActionState とは? ・ Renames useFormState to useActionState ・ Adds a pending state

    to the returned tuple ・ Moves the hook to the 'react' package 【A】useActionState が生える 【3】Server Action に関する近況アップデート https://github.com/facebook/react/pull/28491
  33. ▪ useFormState と useActionState の違い const [formState, formDispatch] = useFormState(updateAction, initialFormState);

    const [isPending] = useFormStatus(); 【A】useActionState が生える 【3】Server Action に関する近況アップデート isPending の参照のために useFormStatus を併用する必要がある
  34. ▪ useFormState と useActionState の違い const [formState, formDispatch, isPending] = useActionState(updateAction,

    initialFormState); 【A】useActionState が生える 【3】Server Action に関する近況アップデート useActionStatus では戻り値に isPending が含まれる
  35. ▪ useFormState -> useActionState なぜ? ・ 実態として、Form ではなくAction の状態を参照している ・ ReactDOM への依存はない ・ "isPending"

    を Action と関連づけることで混乱を避ける 【A】useActionState が生える 【3】Server Action に関する近況アップデート https://github.com/facebook/react/pull/28491
  36. 今後は useActionState を使用した方が望ましい ▪ 書籍では useFormState のサンプルを多数掲載していますが、、、、 ・ useFormState が非推奨 というわけではない ・ ただし

    useActionState を優先するように促される見込み ・ 実装詳細に関しては、サンプルに大きくは影響なし 【A】useActionState が生える 【3】Server Action に関する近況アップデート
  37. 【B】Parallel Routes のバグが解消 【3】Server Action に関する近況アップデート ▪ Parallel Routes モーダル内の Server

    Action バグ ・ 実は、開いたモーダル内で「いいね」を押下するとクラッシュする ・ バグが修正されることを期待して、書籍サンプルはそのままとした
  38. 【B】Parallel Routes のバグが解消 【3】Server Action に関する近況アップデート ▪ Parallel Routes モーダル内の Server

    Action バグ ・ 実は、開いたモーダル内で「いいね」を押下するとクラッシュする ・ バグが修正されることを期待して、書籍サンプルはそのままとした ・ v14.2.2 現在、このバグは解消
  39. すでにリポジトリをクローンされた方は、 pull & 再インストールをお願いします ▪ Parallel Routes モーダル内の Server Action バグ

    ・ 実は、開いたモーダル内で「いいね」を押下するとクラッシュする ・ バグが修正されることを期待して、書籍サンプルはそのままとした ・ v14.2.2 現在、このバグは解消 【B】Parallel Routes のバグが解消 【3】Server Action に関する近況アップデート