-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7ed234e
commit add192b
Showing
3 changed files
with
129 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { Prisma } from "@prisma/client"; | ||
import * as v from "valibot"; | ||
|
||
export type ActionOkResult<T> = T & { success: true }; | ||
export type ActionErrorResult = { success: false; message: string }; | ||
|
||
function actionOkResult<V>(value: V): ActionOkResult<V> { | ||
return { success: true as const, ...value }; | ||
} | ||
|
||
function actionErrorResult(message: string): ActionErrorResult { | ||
return { success: false as const, message }; | ||
} | ||
|
||
export type ServerActionResult<T> = Promise< | ||
ActionOkResult<T> | ActionErrorResult | ||
>; | ||
|
||
export class CatchableError extends Error {} | ||
|
||
export function buildActionFromSchema< | ||
T extends v.ObjectEntries, | ||
U extends v.ErrorMessage<v.ObjectIssue> | undefined, | ||
V, | ||
>( | ||
formSchema: v.ObjectSchema<T, U>, | ||
actionFn: (parsed: v.InferOutput<typeof formSchema>) => Promise<V>, | ||
) { | ||
return async (formData: FormData) => { | ||
const formObj = Object.fromEntries(formData.entries()); | ||
const parseResult = v.safeParse(formSchema, formObj); | ||
if (!parseResult.success) { | ||
console.error(parseResult.issues); | ||
return actionErrorResult( | ||
parseResult.issues.map((i) => i.message).join("\n"), | ||
); | ||
} | ||
|
||
try { | ||
const result = await actionFn(parseResult.output); | ||
return actionOkResult(result); | ||
} catch (err) { | ||
if (err instanceof Prisma.PrismaClientKnownRequestError) { | ||
console.error(`Prisma error: ${err.code}, ${err.message}`); | ||
} else { | ||
console.error(err); | ||
} | ||
|
||
if (err instanceof CatchableError) { | ||
return actionErrorResult(err.message); | ||
} | ||
|
||
return actionErrorResult("不明なエラーが発生しました。"); | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { useEffect, useRef } from "react"; | ||
|
||
export function usePrevious<T>(state: T): T | undefined { | ||
const ref = useRef<T>(); | ||
|
||
useEffect(() => { | ||
ref.current = state; | ||
}); | ||
|
||
return ref.current; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { useRouter } from "next/navigation"; | ||
import { useCallback, useTransition } from "react"; | ||
// import { useToast } from "@/components/ui/use-toast"; | ||
import type { ActionOkResult, ServerActionResult } from "./action-factory"; | ||
|
||
type Options<T> = { | ||
serverActionFn: (formData: FormData) => ServerActionResult<T>; | ||
/** @default '成功しました' */ | ||
successToastMessage?: string; | ||
onSuccess?: (result: ActionOkResult<T>) => void; | ||
onError?: () => void; | ||
hiddenFields?: { [key: string]: string | undefined }; | ||
}; | ||
|
||
export function useServerAction<T>({ | ||
serverActionFn, | ||
// successToastMessage = "成功しました", | ||
onSuccess, | ||
onError, | ||
hiddenFields = {}, | ||
}: Options<T>) { | ||
const router = useRouter(); | ||
// const { toast } = useToast(); | ||
const [isPending, startTransition] = useTransition(); | ||
|
||
const action = useCallback( | ||
(formData: FormData) => { | ||
for (const [key, value] of Object.entries(hiddenFields ?? {})) { | ||
if (value === undefined) continue; | ||
|
||
formData.set(key, value); | ||
} | ||
|
||
startTransition(async () => { | ||
const result = await serverActionFn(formData); | ||
if (!result.success) { | ||
// toast({ | ||
// title: "エラー", | ||
// description: result.message, | ||
// variant: "destructive", | ||
// }); | ||
onError?.(); | ||
return; | ||
} | ||
|
||
// toast({ title: successToastMessage }); | ||
router.refresh(); | ||
onSuccess?.(result); | ||
}); | ||
}, | ||
[ | ||
onError, | ||
onSuccess, | ||
router, | ||
serverActionFn, | ||
hiddenFields, | ||
// toast | ||
], | ||
); | ||
|
||
return [action, isPending] as const; | ||
} |