Skip to content

Commit

Permalink
add use-server-action
Browse files Browse the repository at this point in the history
  • Loading branch information
yo-iwamoto committed Jul 9, 2024
1 parent 7ed234e commit add192b
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 0 deletions.
56 changes: 56 additions & 0 deletions src/lib/action-factory.ts
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("不明なエラーが発生しました。");
}
};
}
11 changes: 11 additions & 0 deletions src/lib/use-previous.ts
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;
}
62 changes: 62 additions & 0 deletions src/lib/use-server-action.ts
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;
}

0 comments on commit add192b

Please sign in to comment.