Skip to content

Commit 62e15bc

Browse files
committed
feat: 状態管理を改善
1 parent fd95e30 commit 62e15bc

2 files changed

Lines changed: 124 additions & 47 deletions

File tree

src/features/admin/auth/login-form.tsx

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { type FormEvent, useState } from "react";
3+
import { SyntheticEvent, useReducer } from "react";
44
import { useRouter } from "next/navigation";
55

66
import { Button } from "@/components/ui/Button";
@@ -17,40 +17,75 @@ function sanitizeRedirectTo(redirectTo: string | undefined, fallback = "/admin")
1717
return redirectTo.startsWith("/") && !redirectTo.startsWith("//") ? redirectTo : fallback;
1818
}
1919

20+
type State = {
21+
email: string;
22+
password: string;
23+
isPending: boolean;
24+
errorMessage: string | null;
25+
};
26+
27+
type Action =
28+
| { type: "SET_EMAIL"; payload: string }
29+
| { type: "SET_PASSWORD"; payload: string }
30+
| { type: "SET_PENDING"; payload: boolean }
31+
| { type: "SET_ERROR"; payload: string | null };
32+
33+
const initialState: State = {
34+
email: "",
35+
password: "",
36+
isPending: false,
37+
errorMessage: null,
38+
};
39+
40+
function reducer(state: State, action: Action): State {
41+
switch (action.type) {
42+
case "SET_EMAIL":
43+
return { ...state, email: action.payload };
44+
case "SET_PASSWORD":
45+
return { ...state, password: action.payload };
46+
case "SET_PENDING":
47+
return { ...state, isPending: action.payload };
48+
case "SET_ERROR":
49+
return { ...state, errorMessage: action.payload };
50+
default:
51+
return state;
52+
}
53+
}
54+
2055
export function LoginForm({ redirectTo }: { redirectTo?: string }) {
2156
const router = useRouter();
22-
const [email, setEmail] = useState("");
23-
const [password, setPassword] = useState("");
24-
const [isPending, setIsPending] = useState(false);
25-
const [errorMessage, setErrorMessage] = useState<string | null>(null);
57+
const [state, dispatch] = useReducer(reducer, initialState);
2658

27-
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
59+
async function handleSubmit(event: SyntheticEvent<HTMLFormElement>) {
2860
event.preventDefault();
2961
const safeRedirectTo = sanitizeRedirectTo(redirectTo);
30-
const normalizedEmail = email.trim();
62+
const normalizedEmail = state.email.trim();
3163

32-
if (!normalizedEmail || !password) {
33-
setErrorMessage("ログインに失敗しました");
64+
if (!normalizedEmail || !state.password) {
65+
dispatch({ type: "SET_ERROR", payload: "ログインに失敗しました" });
3466
return;
3567
}
3668

3769
const supabase = createClient();
38-
setIsPending(true);
39-
setErrorMessage(null);
70+
dispatch({ type: "SET_PENDING", payload: true });
71+
dispatch({ type: "SET_ERROR", payload: null });
4072

4173
try {
4274
const { error } = await supabase.auth.signInWithPassword({
4375
email: normalizedEmail,
44-
password,
76+
password: state.password,
4577
});
4678
if (error) {
4779
throw error;
4880
}
4981
router.push(safeRedirectTo);
5082
} catch (error: unknown) {
51-
setErrorMessage(error instanceof Error ? error.message : "ログインに失敗しました");
83+
dispatch({
84+
type: "SET_ERROR",
85+
payload: error instanceof Error ? error.message : "ログインに失敗しました",
86+
});
5287
} finally {
53-
setIsPending(false);
88+
dispatch({ type: "SET_PENDING", payload: false });
5489
}
5590
}
5691

@@ -76,31 +111,31 @@ export function LoginForm({ redirectTo }: { redirectTo?: string }) {
76111
label="メールアドレス"
77112
placeholder="admin@example.com"
78113
autoComplete="email"
79-
value={email}
80-
onChange={setEmail}
114+
value={state.email}
115+
onChange={(value) => dispatch({ type: "SET_EMAIL", payload: value })}
81116
isRequired
82117
/>
83118
<TextField
84119
name="password"
85120
type="password"
86121
label="パスワード"
87122
autoComplete="current-password"
88-
value={password}
89-
onChange={setPassword}
123+
value={state.password}
124+
onChange={(value) => dispatch({ type: "SET_PASSWORD", payload: value })}
90125
isRequired
91126
/>
92127
<p
93128
role="alert"
94129
aria-live="polite"
95130
className={
96-
errorMessage
131+
state.errorMessage
97132
? "rounded-md border border-red-500/40 bg-red-500/15 px-3 py-2 text-sm text-red-200"
98133
: "min-h-6 text-sm leading-6 text-transparent"
99134
}
100135
>
101-
{errorMessage ?? "\u00a0"}
136+
{state.errorMessage ?? "\u00a0"}
102137
</p>
103-
<Button type="submit" className="h-11 w-full font-medium" isPending={isPending}>
138+
<Button type="submit" className="h-11 w-full font-medium" isPending={state.isPending}>
104139
ログイン
105140
</Button>
106141
<p className="text-center text-sm leading-relaxed text-zinc-300">

src/features/admin/auth/sign-up-form.tsx

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { type FormEvent, useState } from "react";
3+
import { SyntheticEvent, useReducer } from "react";
44
import { useRouter } from "next/navigation";
55

66
import { Button } from "@/components/ui/Button";
@@ -9,42 +9,84 @@ import { Link } from "@/components/ui/Link";
99
import { TextField } from "@/components/ui/TextField";
1010
import { createClient } from "@/lib/supabase/client";
1111

12+
type State = {
13+
email: string;
14+
password: string;
15+
repeatPassword: string;
16+
isPending: boolean;
17+
errorMessage: string | null;
18+
};
19+
20+
type Action =
21+
| { type: "SET_EMAIL"; payload: string }
22+
| { type: "SET_PASSWORD"; payload: string }
23+
| { type: "SET_REPEAT_PASSWORD"; payload: string }
24+
| { type: "SET_PENDING"; payload: boolean }
25+
| { type: "SET_ERROR"; payload: string | null };
26+
27+
const initialState: State = {
28+
email: "",
29+
password: "",
30+
repeatPassword: "",
31+
isPending: false,
32+
errorMessage: null,
33+
};
34+
35+
function reducer(state: State, action: Action): State {
36+
switch (action.type) {
37+
case "SET_EMAIL":
38+
return { ...state, email: action.payload };
39+
case "SET_PASSWORD":
40+
return { ...state, password: action.payload };
41+
case "SET_REPEAT_PASSWORD":
42+
return { ...state, repeatPassword: action.payload };
43+
case "SET_PENDING":
44+
return { ...state, isPending: action.payload };
45+
case "SET_ERROR":
46+
return { ...state, errorMessage: action.payload };
47+
default:
48+
return state;
49+
}
50+
}
51+
1252
export function SignUpForm() {
1353
const router = useRouter();
14-
const [email, setEmail] = useState("");
15-
const [password, setPassword] = useState("");
16-
const [repeatPassword, setRepeatPassword] = useState("");
17-
const [isPending, setIsPending] = useState(false);
18-
const [errorMessage, setErrorMessage] = useState<string | null>(null);
54+
const [state, dispatch] = useReducer(reducer, initialState);
1955

20-
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
56+
async function handleSubmit(event: SyntheticEvent<HTMLFormElement>) {
2157
event.preventDefault();
22-
const normalizedEmail = email.trim();
58+
const normalizedEmail = state.email.trim();
2359

24-
if (password !== repeatPassword) {
25-
setErrorMessage("パスワードが一致しません");
60+
if (state.password !== state.repeatPassword) {
61+
dispatch({ type: "SET_ERROR", payload: "パスワードが一致しません" });
2662
return;
2763
}
2864

29-
if (!normalizedEmail || !password) {
30-
setErrorMessage("アカウント登録に失敗しました");
65+
if (!normalizedEmail || !state.password) {
66+
dispatch({ type: "SET_ERROR", payload: "アカウント登録に失敗しました" });
3167
return;
3268
}
3369

3470
const supabase = createClient();
35-
setIsPending(true);
36-
setErrorMessage(null);
71+
dispatch({ type: "SET_PENDING", payload: true });
72+
dispatch({ type: "SET_ERROR", payload: null });
3773

3874
try {
39-
const { error } = await supabase.auth.signUp({ email: normalizedEmail, password });
75+
const { error } = await supabase.auth.signUp({
76+
email: normalizedEmail,
77+
password: state.password,
78+
});
4079
if (error) {
4180
throw error;
4281
}
4382
router.push("/auth/sign-up-success");
4483
} catch (error: unknown) {
45-
setErrorMessage(error instanceof Error ? error.message : "アカウント登録に失敗しました");
84+
dispatch({
85+
type: "SET_ERROR",
86+
payload: error instanceof Error ? error.message : "アカウント登録に失敗しました",
87+
});
4688
} finally {
47-
setIsPending(false);
89+
dispatch({ type: "SET_PENDING", payload: false });
4890
}
4991
}
5092

@@ -70,40 +112,40 @@ export function SignUpForm() {
70112
label="メールアドレス"
71113
placeholder="admin@example.com"
72114
autoComplete="email"
73-
value={email}
74-
onChange={setEmail}
115+
value={state.email}
116+
onChange={(value) => dispatch({ type: "SET_EMAIL", payload: value })}
75117
isRequired
76118
/>
77119
<TextField
78120
name="password"
79121
type="password"
80122
label="パスワード"
81123
autoComplete="new-password"
82-
value={password}
83-
onChange={setPassword}
124+
value={state.password}
125+
onChange={(value) => dispatch({ type: "SET_PASSWORD", payload: value })}
84126
isRequired
85127
/>
86128
<TextField
87129
name="repeatPassword"
88130
type="password"
89131
label="パスワード(確認)"
90132
autoComplete="new-password"
91-
value={repeatPassword}
92-
onChange={setRepeatPassword}
133+
value={state.repeatPassword}
134+
onChange={(value) => dispatch({ type: "SET_REPEAT_PASSWORD", payload: value })}
93135
isRequired
94136
/>
95137
<p
96138
role="alert"
97139
aria-live="polite"
98140
className={
99-
errorMessage
141+
state.errorMessage
100142
? "rounded-md border border-red-500/40 bg-red-500/15 px-3 py-2 text-sm text-red-200"
101143
: "min-h-6 text-sm leading-6 text-transparent"
102144
}
103145
>
104-
{errorMessage ?? "\u00a0"}
146+
{state.errorMessage ?? "\u00a0"}
105147
</p>
106-
<Button type="submit" className="h-11 w-full font-medium" isPending={isPending}>
148+
<Button type="submit" className="h-11 w-full font-medium" isPending={state.isPending}>
107149
新規登録
108150
</Button>
109151
<p className="text-center text-sm leading-relaxed text-zinc-300">

0 commit comments

Comments
 (0)