Skip to content

Commit d6b0b85

Browse files
committed
Simplify form mutations
1 parent 74d4f06 commit d6b0b85

File tree

5 files changed

+47
-26
lines changed

5 files changed

+47
-26
lines changed

src/components/controls/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const Button = forwardRef(({ className, color, loading, children, disabled, ...p
3939
return (
4040
<button ref={ref} className={computedClassName} disabled={loading || disabled} {...props}>
4141
{!loading && children}
42-
<AnimatePresence mode="wait">
42+
<AnimatePresence>
4343
{loading && <motion.span
4444
key="placeholder"
4545
className="invisible"

src/components/pages/contribute/FeedbackSection.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import TextArea from "@/components/controls/TextArea";
1212
import Form from "@/components/controls/Form";
1313
import Heading from "@/components/layout/Heading";
1414
import Label from "@/components/controls/Label";
15+
import useFormMutation from "@/hooks/useMutation";
1516

1617
const feedbackSchema = z.object({
1718
text: z.string().min(1, "Your feedback message must at least contain 1 character")
@@ -27,21 +28,12 @@ const FeedbackSection = () => {
2728
mode: "onChange"
2829
});
2930

30-
const [response, setResponse] = useState<null | BackendResponse>(null);
31-
const [loading, setLoading] = useState(false);
32-
33-
const loadingRef = useRef(loading);
34-
loadingRef.current = loading;
35-
36-
const submit = useMemo(() => handleSubmit(({ text }) => {
37-
if (loadingRef.current) return;
38-
setLoading(true);
39-
31+
const [submit, response, loading] = useFormMutation<BackendResponse, FeedbackSchema>(handleSubmit, (setResponse, { text }) => {
4032
backend.url("/feedback")
4133
.post({ text })
4234
.json((res: BackendResponse) => setResponse(res));
43-
}), [handleSubmit]);
44-
35+
});
36+
4537
return (
4638
<section
4739
aria-labelledby="feedback"

src/components/pages/front/SignupSection.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Form from "@/components/controls/Form";
1313
import Heading from "@/components/layout/Heading";
1414

1515
import { backend } from "@/utils/wretch";
16+
import useFormMutation from "@/hooks/useMutation";
1617

1718
const signupSchema = z.object({
1819
email: z.string().email().min(5, "Your email must at least contain 5 characters")
@@ -27,20 +28,11 @@ const SignupSection = () => {
2728
mode: "onChange"
2829
});
2930

30-
const [response, setResponse] = useState<null | BackendResponse>(null);
31-
const [loading, setLoading] = useState(false);
32-
33-
const loadingRef = useRef(loading);
34-
loadingRef.current = loading;
35-
36-
const submit = useMemo(() => handleSubmit(({ email }) => {
37-
if (loadingRef.current) return;
38-
setLoading(true);
39-
31+
const [submit, response, loading] = useFormMutation<BackendResponse, SignupSchema>(handleSubmit, (setResponse, { email }) => {
4032
backend.url("/email/subscribe")
4133
.post({ email })
4234
.json((res: BackendResponse) => setResponse(res));
43-
}), [handleSubmit]);
35+
});
4436

4537
return (
4638
<section

src/hooks/useMutation.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from "react";
2+
import { UseFormHandleSubmit, FieldValues } from "react-hook-form";
3+
4+
export type MutationHandler<TResponse> = Dispatch<SetStateAction<TResponse | null>>;
5+
6+
const useFormMutation = <TResponse extends {}, TFieldValues extends FieldValues = FieldValues, THandleSubmit extends UseFormHandleSubmit<TFieldValues> = UseFormHandleSubmit<TFieldValues>>(
7+
handleSubmit: THandleSubmit,
8+
handler: (setResponse: MutationHandler<TResponse>, data: TFieldValues, event?: React.BaseSyntheticEvent) => unknown | Promise<unknown>
9+
) => {
10+
11+
const [response, setResponse] = useState<null | TResponse>(null);
12+
const [loading, setLoading] = useState(false);
13+
14+
const handlerRef = useRef(handler);
15+
handlerRef.current = handler;
16+
17+
const handleSubmitRef = useRef<THandleSubmit>(handleSubmit);
18+
handleSubmitRef.current = handleSubmit;
19+
20+
const loadingRef = useRef(loading);
21+
loadingRef.current = loading;
22+
23+
const mutate = useMemo(() => handleSubmitRef.current(async (...vals) => {
24+
if (loadingRef.current) return;
25+
setLoading(true);
26+
27+
await handlerRef.current(
28+
(...setterValues) => {
29+
setResponse(...setterValues);
30+
setLoading(false);
31+
},
32+
...vals
33+
);
34+
}), []);
35+
36+
return [mutate, response, loading] as const;
37+
};
38+
39+
export default useFormMutation;

src/pages/mail/unsubscribe.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { z } from "zod";
44
import { zodResolver } from "@hookform/resolvers/zod";
55
import { useForm } from "react-hook-form";
66
import ArrowPathIcon from "@heroicons/react/24/solid/ArrowPathIcon";
7-
import { AnimatePresence, motion } from "framer-motion";
87

98
import Button from "@/components/controls/Button";
109
import { Page } from "@/types/page";
@@ -15,7 +14,6 @@ import Form from "@/components/controls/Form";
1514
import Heading from "@/components/layout/Heading";
1615
import { makeOgMeta } from "@/utils/meta/opengraph";
1716
import { makeSitemapMeta } from "@/utils/meta/sitemap";
18-
import Label from "@/components/controls/Label";
1917

2018
const unsubscribeSchema = z.object({
2119
email: z.string().email().min(3)

0 commit comments

Comments
 (0)