はじめに
Next.js + Supabaseで農業向け栽培管理アプリを開発しています。
今回はデモ版の書き込み制御と、通知UIをsonnerに統一した話です。
デモ版の仕様
栽培管理アプリにはデモ版を用意しています。
デモ版の仕様はシンプルで、閲覧はできるけど書き込みはできない、というものです。
ただ最初の実装のままだと、書き込みボタンを押したときに
登録に失敗しました。もう一度お試しください。
という普通のエラーメッセージが出ていました。
ユーザーからすると「システムの不具合なのか」「デモの制限なのか」が分かりません。これはよくない。
そこで今回は2つの改善をしました。
- 通知UIをsonnerに統一する
- デモ版の書き込みをフロント側で止める
問題①:通知UIがバラバラだった
最初の実装では、各ページに
const [message, setMessage] = useState("")
のような状態管理を作って、登録成功やエラーをそれぞれ表示していました。
これだとページごとに表示位置もデザインもバラバラになるし、同じような実装を何度も書く羽目になります。
sonnerを導入する
そこでsonnerを導入しました。React向けのトースト通知ライブラリです。
sonnerをインストールする
ターミナルで実行。
npm install sonner
あとはlayout.tsx に <Toaster /> を追加するだけで、全ページ共通の通知UIが使えるようになります。
// src/app/layout.tsx
import { Toaster } from "sonner";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Toaster position="top-center" richColors />
</body>
</html>
);
}
通知ユーティリティを作る
さらに使いやすくするため、共通のユーティリティファイルを作りました。
// src/utils/notify.ts
import { toast } from "sonner";
export const notify = {
success: (msg: string) => toast.success(msg),
error: (msg: string) => toast.error(msg),
info: (msg: string) => toast(msg),
demoOnly: () =>
toast.info(
"デモ版では閲覧のみご利用いただけます。導入をご検討の方はお問い合わせください。"
),
};
これで各ページでは
notify.success("保存しました")
notify.error("エラーが発生しました")
notify.demoOnly()
と書くだけで済みます。表示位置もデザインも統一されました。
問題②:書き込みをDBレベルで止めていた
当初はSupabaseのRLS(Row Level Security)でINSERT・UPDATE・DELETEをブロックしていました。DBレベルで書き込みを禁止する方法です。
ただこの方法だと、ユーザーが保存ボタンを押したとき、RLSが拒否した結果として
登録に失敗しました
という本物のエラーに見えてしまいます。「デモ版の制限です」とは伝わらない。
フロント側でデモ判定する
そこでボタンを押した瞬間にデモかどうかを判定して、デモなら処理を止めてトーストを表示する方式に変えました。
クリック
↓
デモ判定
↓(デモの場合)
処理を止める → notify.demoOnly() を表示
↓(デモでない場合)
通常の保存処理へ
判定用のユーティリティはこれだけです。
// src/utils/demo.ts
import { notify } from "./notify";
export function blockIfDemo() {
if (process.env.NEXT_PUBLIC_DEMO_MODE === "true") {
notify.demoOnly();
return true;
}
return false;
}
NEXT_PUBLIC_DEMO_MODE はVercelのデモ用プロジェクトにだけ設定している環境変数です。本番プロジェクトには設定しないので、本番では何も起きません。
使い方はシンプルで、保存処理の前に1行挟むだけです。
if (blockIfDemo()) return;
await createHarvestAction(data);
これでデモ版では保存ボタンを押すと
デモ版では閲覧のみご利用いただけます。導入をご検討の方はお問い合わせください。
というトーストが出て、処理は止まります。エラーではなく「制限です」と伝わる形になりました。
おまけ:デモ確認中に別のバグも見つかった
デモ版の動作確認をしているときに、別の問題も見つかりました。
収穫登録フォームに「収穫量(任意)」という項目があります。入力しなくても保存できるようにしていたつもりだったんですが、空のまま保存するとエラーになる。
本番環境では
Server Components error
というメッセージが出ていました。
調べてみるとSupabaseのテーブル定義が
harvest_amount numeric NOT NULL
になっていました。UIでは任意にしているのに、DBでは必須になっている。典型的な設計の不整合です。
Supabaseの SQL Editor で1行直して解決しました。
alter table cult_harvests
alter column harvest_amount drop not null;
Next.jsの本番で出る Server Components error は原因が分かりにくいことが多いですが、フォームの入力→Server Action→DB制約の流れのどこかで詰まっているケースが多いです。DB定義も確認する癖をつけておくと解決が早いです。
まとめ
今回やったことをまとめると:
- 通知UIをsonnerに統一 → 表示位置・デザインが揃った
- デモの書き込み制御をフロントに移した → 「エラー」ではなく「制限」として伝わるようになった
- 収穫量のNOT NULL制約を外した → 任意入力が本当に任意になった
デモ版は「触ってもらうためのもの」なので、ユーザーが戸惑わない動きになっているかは地味に大切です。今回の改善でだいぶ整った気がします。
