Last active
October 11, 2025 15:05
-
-
Save nakanoasaservice/67fca619d58113a2518781269bfb7973 to your computer and use it in GitHub Desktop.
Next.jsでトーストライブラリsonnerをServer Actionsから呼べるようにするラッパー
This file contains hidden or 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
| "use client" | |
| import { usePathname } from "next/navigation" | |
| import { useEffect } from "react" | |
| import { toast } from "sonner" | |
| import * as v from "valibot" | |
| import { TOAST_COOKIE_NAME, ToastCommandSchema } from "./commons" | |
| /** | |
| * クッキーを取得する | |
| * @param name クッキー名 | |
| * @returns クッキーの値(存在しない場合はnull) | |
| */ | |
| function getCookie(name: string): string | null { | |
| try { | |
| if (typeof document === "undefined") return null | |
| const cookies = document.cookie.split("; ") | |
| const cookie = cookies.find((c) => c.startsWith(`${name}=`)) | |
| if (!cookie) return null | |
| // "name=value" 形式から "value" 部分を取得 | |
| return decodeURIComponent(cookie.substring(name.length + 1)) | |
| } catch (error) { | |
| console.error(`[トースト] クッキー「${name}」の取得に失敗しました:`, error) | |
| return null | |
| } | |
| } | |
| /** | |
| * クッキーを削除する | |
| * @param name クッキー名 | |
| */ | |
| function deleteCookie(name: string): void { | |
| try { | |
| if (typeof document === "undefined") return | |
| // 有効期限を過去に設定することでクッキーを削除 | |
| // eslint-disable-next-line functional/immutable-data -- クッキーを削除するため | |
| document.cookie = `${name}=; path=/; max-age=0` | |
| } catch (error) { | |
| console.error(`[トースト] クッキー「${name}」の削除に失敗しました:`, error) | |
| } | |
| } | |
| /** | |
| * トーストメッセージを処理して表示する | |
| * @param cookieValue クッキーの値 | |
| */ | |
| function processToastMessage(cookieValue: string): void { | |
| try { | |
| const parsedValue = JSON.parse(cookieValue) as unknown | |
| const command = v.safeParse(ToastCommandSchema, parsedValue) | |
| if (!command.success) { | |
| console.error( | |
| "[トースト] メッセージのバリデーションに失敗しました:", | |
| command.issues, | |
| ) | |
| return | |
| } | |
| const { type, message, data } = command.output | |
| if (!type) { | |
| toast(message, data) | |
| } else { | |
| toast[type](message, data) | |
| } | |
| } catch (parseError) { | |
| if (parseError instanceof SyntaxError) { | |
| console.error( | |
| "[トースト] メッセージのJSONパースに失敗しました:", | |
| parseError, | |
| ) | |
| } else { | |
| console.error( | |
| "[トースト] メッセージの処理中に予期しないエラーが発生しました:", | |
| parseError, | |
| ) | |
| } | |
| // フォールバックとして基本的なエラーメッセージを表示 | |
| toast.error("通知の表示中にエラーが発生しました") | |
| } | |
| } | |
| /** | |
| * サーバーからのトーストメッセージを表示するクライアントコンポーネント | |
| * パスが変更されるたびにクッキーからメッセージを取得して表示します | |
| */ | |
| export function RedirectToast() { | |
| const pathname = usePathname() | |
| useEffect(() => { | |
| try { | |
| // クライアントサイドでクッキーを取得 | |
| const toastCookieValue = getCookie(TOAST_COOKIE_NAME) | |
| if (!toastCookieValue) return | |
| // クッキーを削除 | |
| deleteCookie(TOAST_COOKIE_NAME) | |
| // トーストメッセージの処理と表示 | |
| processToastMessage(toastCookieValue) | |
| } catch (error) { | |
| console.error("[トースト] 処理中に予期しないエラーが発生しました:", error) | |
| } | |
| }, [pathname]) // パスが変わるたびに実行 | |
| // 実際にはUIを描画しないコンポーネント | |
| return null | |
| } |
This file contains hidden or 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
| import * as v from "valibot" | |
| export const TOAST_COOKIE_NAME = "redirect-toast" | |
| export type RedirectToastMessage = string | number | boolean | |
| export const RedirectToastMessageSchema = v.union([ | |
| v.string(), | |
| v.number(), | |
| v.boolean(), | |
| ]) | |
| const ToastTypeSchema = v.picklist([ | |
| "success", | |
| "info", | |
| "warning", | |
| "error", | |
| "loading", | |
| ]) | |
| export type ToastType = v.InferOutput<typeof ToastTypeSchema> | |
| export const ToastDataSchema = v.object({ | |
| id: v.optional(v.union([v.string(), v.number()])), | |
| richColors: v.optional(v.boolean()), | |
| invert: v.optional(v.boolean()), | |
| closeButton: v.optional(v.boolean()), | |
| dismissible: v.optional(v.boolean()), | |
| duration: v.optional(v.number()), | |
| description: v.optional(RedirectToastMessageSchema), | |
| delete: v.optional(v.boolean()), | |
| position: v.optional( | |
| v.picklist(["top-left", "top-right", "bottom-left", "bottom-right"]), | |
| ), | |
| }) | |
| export type ToastData = v.InferOutput<typeof ToastDataSchema> | |
| export const ToastCommandSchema = v.object({ | |
| type: v.optional(ToastTypeSchema), | |
| message: RedirectToastMessageSchema, | |
| data: v.optional(ToastDataSchema), | |
| }) | |
| export type ToastCommand = v.InferOutput<typeof ToastCommandSchema> |
This file contains hidden or 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
| import "server-only" | |
| import { cookies } from "next/headers" | |
| import { | |
| type RedirectToastMessage, | |
| TOAST_COOKIE_NAME, | |
| type ToastCommand, | |
| type ToastData, | |
| type ToastType, | |
| } from "./commons" | |
| const createRedirectToast = | |
| (type?: ToastType) => | |
| async (message: RedirectToastMessage, data?: ToastData) => { | |
| try { | |
| const command: ToastCommand = { | |
| type, | |
| message, | |
| data, | |
| } | |
| const cookieStore = await cookies() | |
| cookieStore.set(TOAST_COOKIE_NAME, JSON.stringify(command), { | |
| secure: true, | |
| sameSite: "strict", | |
| httpOnly: false, // クライアントからアクセスできるようにfalse | |
| path: "/", | |
| maxAge: 30, // 30秒 | |
| }) | |
| } catch (error) { | |
| console.error( | |
| `[トースト] 通知の設定に失敗しました: ${error instanceof Error ? error.message : String(error)}`, | |
| ) | |
| // 必要に応じてエラーモニタリングサービスに送信するなどの処理を追加 | |
| } | |
| } | |
| type RedirectToast = ReturnType<typeof createRedirectToast> & { | |
| [key in ToastType]: ReturnType<typeof createRedirectToast> | |
| } | |
| export const redirectToast = createRedirectToast() as RedirectToast | |
| // eslint-disable-next-line functional/immutable-data -- sonnerのAPIをサーバー上で再現するため | |
| Object.assign(redirectToast, { | |
| success: createRedirectToast("success"), | |
| error: createRedirectToast("error"), | |
| info: createRedirectToast("info"), | |
| warning: createRedirectToast("warning"), | |
| loading: createRedirectToast("loading"), | |
| } satisfies { [key in ToastType]: ReturnType<typeof createRedirectToast> }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment