Skip to content
This repository has been archived by the owner on Sep 17, 2024. It is now read-only.

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
nonoakij committed May 22, 2024
1 parent 9d32c13 commit bac70c1
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 38 deletions.
2 changes: 1 addition & 1 deletion apps/solution/app/components/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function LoginForm() {
</div>
</CardContent>
<CardFooter>
<Button>登録する</Button>
<Button>ログインする</Button>
</CardFooter>
</form>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useFormState } from "react-dom";

export function SignInForm() {
export function SignUpForm() {
const [state, formAction] = useFormState(auth, null);
return (
<form action={formAction}>
Expand Down Expand Up @@ -36,7 +36,7 @@ export function SignInForm() {
</div>
</CardContent>
<CardFooter>
<Button>ログインする</Button>
<Button>登録する</Button>
</CardFooter>
</form>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/solution/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LoginForm } from "@/app/components/login-form";
import { SignInForm } from "@/app/components/sign-in-form";
import { SignUpForm } from "@/app/components/sign-up-form";
import {
Card,
CardDescription,
Expand Down Expand Up @@ -32,7 +32,7 @@ export default function Home(props: {
をご利用ください!
</CardDescription>
</CardHeader>
<SignInForm />
<SignUpForm />
</Card>
</TabsContent>
<TabsContent value="login">
Expand Down
2 changes: 1 addition & 1 deletion apps/workspace/app/components/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function LoginForm() {
</div>
</CardContent>
<CardFooter>
<Button>登録する</Button>
<Button>ログインする</Button>
</CardFooter>
</form>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CardContent, CardFooter } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

export function SignInForm() {
export function SignUpForm() {
return (
<form>
<CardContent className="space-y-2">
Expand All @@ -22,7 +22,7 @@ export function SignInForm() {
</div>
</CardContent>
<CardFooter>
<Button>ログインする</Button>
<Button>登録する</Button>
</CardFooter>
</form>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/workspace/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LoginForm } from "@/app/components/login-form";
import { SignInForm } from "@/app/components/sign-in-form";
import { SignUpForm } from "@/app/components/sign-up-form";
import {
Card,
CardDescription,
Expand Down Expand Up @@ -29,7 +29,7 @@ export default function Home() {
をご利用ください!
</CardDescription>
</CardHeader>
<SignInForm />
<SignUpForm />
</Card>
</TabsContent>
<TabsContent value="login">
Expand Down
Binary file added docs/src/content/assets/validate-result-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/src/content/docs/guides/4-auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ export async function auth(formData: FormData) {
}
```

次に、`apps/workspace/app/pages/sign-up.tsx` を以下のように変更してください。
次に、`apps/workspace/app/components/sign-up-form.tsx` を以下のように変更してください。

```diff lang="tsx"
// apps/workspace/app/pages/sign-up.tsx
// apps/workspace/app/components/sign-up-form.tsx
+ import { auth } from "@/app/actions";
import { Button } from "@/components/ui/button";
import { CardContent, CardFooter } from "@/components/ui/card";
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/5-validation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ import { redirect } from "next/navigation";
+ Object.fromEntries(formData.entries()),
+ );
+ if (!parsedFormData.success) {
+ throw new Error("Invalid form data", { cause: parsedFormData.error })
+ throw new Error("Invalid form data", { cause: parsedFormData.error });
+ }
+
+ const { email, password } = parsedFormData.data;
Expand Down
169 changes: 144 additions & 25 deletions docs/src/content/docs/guides/6-useFormState.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,62 @@
title: 6. 画面にエラーの情報を表示する
description: 6. 画面にエラーの情報を表示する
---
import { Aside } from '@astrojs/starlight/components';

## エラーの情報を表示する
エラーを画面に表示するために、まずは Server Action を変更しましょう。

```diff lang="tsx"
// apps/workspace/app/actions.ts

const authSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
+ type State = inferFlattenedErrors<typeof authSchema> | null;
export async function auth(formData: FormData) {
const parsedFormData = authSchema.safeParse(
Object.fromEntries(formData.entries()),
);
if (!parsedFormData.success) {
+ throw new Error("Invalid form data", { cause: parsedFormData.error });
- return parsedFormData.error.formErrors;
}

- export async function auth(formData: FormData) {
+ export async function auth(state: State, formData: FormData): Promise<State> {
```

このように変更することで、validation に失敗した結果をclient側で取り扱えるようになります。

次に、画面にエラーメッセージを表示するために、`sign-in-form.tsx` を変更して、エラーをStateに保存するようにします。

またエラーを表示するために、クライアントで再レンダリングする必要がある、`use client`をつける必要があります。

```diff lang="tsx"
// apps/workspace/app/components/sign-in-form.tsx
export function SignInForm() {
+ const [state, formAction] = useFormState(auth, null);
// apps/workspace/app/components/sign-up-form.tsx
+"use client";
import { auth } from "@/app/actions";
import { Button } from "@/components/ui/button";

...

export function SignUpForm() {
+ const [state, setState] = useState<{
+ fieldErrors: { email?: string[]; password?: string[] }
+ }>()
+
+ const formAction = async (formData: FormData) => {
+ const res = await auth(formData)
+ if(res !== null){
+ setState(res)
+ }
+ }

return (
- <form action={auth}>
+ <form action={formAction}>
+ <form action={formAction}>

```

あとは、エラーの情報を画面に表示するように変更します。

```diff lang="tsx"
// apps/workspace/app/components/sign-up-form.tsx
...
<CardContent className="space-y-2">
<div className="space-y-1">
<Label htmlFor="email">Email</Label>
Expand All @@ -31,26 +67,109 @@ export function SignInForm() {
type="email"
placeholder="[email protected]"
/>
+ {state?.fieldErrors.email && (
+ <p role="alert" className="text-red-500">
+ {state.fieldErrors.email}
+ </p>
+ )}
+ {state?.fieldErrors.email && (
+ <p role="alert" className="text-red-500">
+ {state.fieldErrors.email}
+ </p>
+ )}
</div>
<div className="space-y-1">
<Label htmlFor="password">Password</Label>
<Input id="password" name="password" type="password" />
+ {state?.fieldErrors.password && (
+ <p role="alert" className="text-red-500">
+ {state.fieldErrors.password}
+ </p>
+ )}
+ {state?.fieldErrors.password && (
+ <p role="alert" className="text-red-500">
+ {state.fieldErrors.password}
+ </p>
+ )}
</div>
</CardContent>
<CardFooter>
<Button>ログインする</Button>
</CardFooter>
</form>
```

ここまで実装できたら、`http://localhost:3000` にアクセスして、再度ログインフォームに下記のような値を入れてみてください。
- email: `foo@example`
- password: "" // 何も入力しない

下記のようにエラーが表示されることを確認してください。
![エラー表示](../../assets/validate-result-ui.png)

## 組み込みのHooksを利用してより簡潔に書く

`useFormState` を利用することで、簡潔に書くことができます。

`useFormState`を使う前に下記のように action を変更しておきましょう。
理由は後述しますので、まずは変更してみてください。

```diff lang="tsx"
// apps/workspace/app/actions.ts

const authSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
+ type State = inferFlattenedErrors<typeof authSchema> | null;

- export async function auth(formData: FormData) {
+ export async function auth(state: State, formData: FormData): Promise<State> {
const parsedFormData = authSchema.safeParse(
Object.fromEntries(formData.entries()),
);
if (!parsedFormData.success) {

```

次に、`useFormState` を利用する形に`sign-in-form.tsx`を変更してみましょう。
```diff lang="tsx"
// apps/workspace/app/components/sign-in-form.tsx

-import { useState } from "react";
+import { useFormState } from "react-dom";

export function SignInForm() {
- const [state, setState] = useState<{
- fieldErrors: { email?: string[]; password?: string[] }
- }>()
-
- const formAction = async (formData: FormData) => {
- const res = await auth(formData)
- if(res !== null){
- setState(res)
- }
- }
+ const [state, formAction] = useFormState(auth, null);
return (
<form action={formAction}>
);
}
```

これで、`useFormState` を利用して、簡潔に書くことができました。
`useFormState` の第一引数には、action を、第二引数にはStateの初期値をとります。
第一引数の action の第一引数には、現在のStateを受け取り、第二引数には、formのデータを受け取ります。
例えば、下記のように書くことができます。

```tsx
const [state, formAction] = useFormState(async (state, formData) => {
// 本当はバリデーションしたほうがいいですが、省略
const count = formData.get('count')
return count + state
}, 0);
```

このため、action.ts を変更したというわけです。

<Aside type="tip">
useFormState は、useActionState に置き換わることになりました。
この Hands-on 作成時点では、Next.jsでの利用はまだサポートされていないため、useFormState を利用しています。
useActionState が利用可能になった際には、そちらを利用することをお勧めします。
https://github.com/facebook/react/pull/28491
</Aside>
<Aside type="tip">
useFormState と似ているものに useFormStatus があります。
詳細はこちらを参照してください。
https://ja.react.dev/reference/react-dom/hooks/useFormStatus
</Aside>

## まとめ

client component を使って、エラーの情報を画面に表示することができます。
また、useFormState Hook を利用することで、簡潔に書くことができます。

0 comments on commit bac70c1

Please sign in to comment.