Hono, TanStack Query練習

ここで作ってく
使う技術
- react(vite)
- tanstack router
- tanstack query
- hono
- supabase

データベース, ORMなど知識皆無なので、全部担ってくれるSupabaseに頼る
honoのapi書くファイル内でsupabaseのjsコード書いてdbアクセスして、て感じでいけそう
apiエンドポイントがあるとは言えさすがにクライアントルーティングは別で用意する必要がありそうなのでtanstack routerも使うことになると思う

ディレクトリ構成はこうなりそう
project
├──front
│ └──src // React
└──back
└──index.tsx // Hono & Supabase
frontでcreate vite
、backでcreate hono
するか

- フロントエンド
npm create vite@latest frontend
- バックエンド
npm create hono@latest backend
honoのテンプレートはcf pagesで良いか(よくわからんが)
PagesとWorkersは機能的にも似通っていてややこしいのですが、ざっくりとPagesは静的コンテンツ向けのホスティングサービスで、シンプルなデプロイや配信に最適で、Workersは動的な処理やサーバーレスアプリに特化した、より高度なカスタマイズが可能なサービスです。
バックエンド側でsupabaseインストール
cd backend/
npm i @supabase/supabase-js

次、TanStack Router導入/セットアップ
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin
@tanstack/router-devtoolsは前使わなかったので良いや
vite.config.tsのimport react from '@vitejs/plugin-react'
は見つからなかったのでimport react from '@vitejs/plugin-react-swc'
に変更

メモ
- Honoのインスタンス作る時に使用するBindingsって型はなんだ
- middlewareっていうのが何なのかわかってない
- 認証とか?でもtanstackのbeforeLoadでsupabase-js書けば認証できちゃうな
- jsxをhonoから返せるっぽい(今回それやるかはわからん)

middlewareは、tanstack routerのbeforeLoadみたいなクライアントルーティングでのpathアクセス前に認証かけたりするやつとは違って、apiエンドポイントアクセス時にそのエンドポイントにおいて何かをreturnする前に認証かけたりするやつっぽい。CORSをhono middlewareで書いたらイメージ掴めた

さて、HonoでAPI書いていく
frontを5000, backを5001ポートで常時立てておく
なのでフロントからhttp://localhost:5001/
のhono apiエンドポイントにfetchする感じかな

http://localhost:5001/api
に変更

c.renderしちゃうとjsxがそのままレンダーされて画面表示されちゃう気がするので、ただレスポンスしてほしいな
レスポンスはどうやってやるんだ?
geminiに聞いた。return c.text
でいけるのか
app.get('/', (c) => {
return c.text('はじめる')
})

んでそれを受け取るフロント側も初めて書くがやってみよう
- まずfetchで得る型は
Promise<Response>
ぽい - それをresponseとして受け取ったら、更に
response.text()
までしてやっと生のテキストとして使える
text() は Response インターフェイスのメソッドで、 Response ストリームを取得して完全に読み込みます。文字列で解決するプロミスを返します。
今回honoからはreturn c.text()
でテキストとして返してるから、jsonとして解釈しようとするrespose.json()
は使えないのでresponse.text()
使うってことか
json() は Response インターフェイスのメソッドで、 Response のストリームを取得して完全に読み取ります。本体のテキストを JSON として解釈した結果で解決するプロミスを返します。
なお、このメソッドは json() という名前であるにもかかわらず、結果は JSON ではありません。入力として JSON を取って解釈し、 JavaScript のオブジェクトを生成します。

フロント側でCORSエラーが出たのでHono側でCORSコードを書く必要もありそうだ
用意されてる、見つけた
originをhttp://localhost:5000/
にしてあげるとかでいけそう
あとTanStack Queryとか使って、use()
に渡すpromiseをキャッシュしろというのをよく聞くのでその導入もやりたい
キャッシュしないとレンダリングごとに毎回fetchして新しくpromise作っちゃうから非効率とかそういう理由だと思う(そんでそこから下手すると無限レンダリングに繋がったりするのかも?)

よし、Honoで以下ミドルウェア書いたら画面描画が理想通り機能した
app.use(
cors({
origin: 'http://localhost:5000',
})
)
だがコンソールにエラーは出ている
まさにこれだな
キャッシュしてないpromiseが見つかったら自動でエラー出して「キャッシュしろ!」て警告を出すReactの仕様だろう
同じくuse()
使ってSupabaseのAPI叩いたときは何もエラー出なかった理由は、supabaseのDB APIにはすでに全origin許可のCORS設定が書かれているのだろう
あーやっぱそうっぽい、これだ。なるほど

とりあえずTanStack Queryを導入してキャッシュしていく
TanStack Query編
npm i @tanstack/react-query
npm i @tanstack/react-query-devtools
supabaseコードもawait使うような非同期処理なのでbackend/内にもインストールすべきか?
いやでも...
Suspense機能させたい
↓
use()
使うしかない
↓
tanstack query導入してキャッシュせねば
という流れなので、クライアント側でSuspenseしちゃえばバックエンドの非同期待ちは拾えるから普通にSuspense機能させることができるのでバックエンドではtanstack query不要か
まぁでもbackend/ではキャッシュできなくなるからちょっと非効率にはなるのかも
なので余裕があったらbackend/でも導入しよう
とりあえずfrontend/で導入していく

セットアップはこちらも参考にできそう(react-queryに関しても書かれてるので)
公式

まずこれ読んでReactQueryDevtools書く
んでこれ読んでQueryClientProviderとかそのあたり書く
既にtanstack routerのRouterProviderもあるのでその同階層にQueryClientProviderを書けば良いのか親に書けば良いのかわからないが以下を参考にすると親で良さそう
公式にも同じように書かれてたのでやはり親で良さそう(なぜqueryの方が親になるのかは気になるが)
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>,

useSuspenseQueryというのがあるっぽいがこれ使えばuse()
使わずに普通にawait使ってfetchしてもSuspense機能しそう...!!!?めっちゃ助かるぞ
無限レンダリング回避するためuse()
使うコンポーネントとfetch()
使うコンポーネント分割しないといけなくていろいろ面倒だったがそれが不要になるのか
いやtanstack query導入したらその分割も不要になる気はするからそれは関係ないかも...
まぁuse()
使わずにfetchだけで良くなるのはどっちみち助かる
そしてuseQueryとほぼ同じ使い方っぽい、素晴らしい
注意点もあるっぽいので一応メモ

useQuery(useSuspenseQuery)では関係ないが、useMutation内でinvalidateQueriesしたい時はqueryClient.invalidateQueries
とやる必要があるのでqueryClientというのが必要になってくる
んでそれはuseQueryClientというのを使えば取得できる
import { useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()

ディレクトリ設計どうすべきだろうか
ん-これ通りservice/というのを作るとtanstack routerで既にパスとして分離できてるディレクトリと被る気がするなぁ
こっち参考にして、普通に-functions/っていうの作ってそこに全部突っ込むか

とりあえずここ以降を参考にして同じように実装してみる
できた!!use()
使わずにawaitしてるだけなのにSuspense機能してる!useSuspenseQuery凄い!
エラーも消えてる
だが1つ気になる点がある
tanstack queryでisPendingの状態を取得する必要があるのかどうかわからない
tanstack routerの方でpendingComponent:
で設定しているので絶対重複してるんだよな
どうすれば良いだろう、どっちを使えば良いんだろう

試してみたら、isPendingの方は完全に機能していないことがわかった
tanstack routerのpendingComponentしか機能してない
pendingComponentを消してisPendingのコンポーネントだけ表示させようとしたらそもそもpending中は画面真っ白になってSuspense機能してなかった
更に....useSuspenseQueryではなくuseQueryにしてみたのだが、それでもなぜか普通にSuspense機能してる
なぜ??気になるがとりあえず飛ばす。pendingComponentを書けばokということをとりあえず暗記する。そして一応useSuspenseQueryの方を使っていくことにする

違いはdataの状態だけであって、どっちもSuspenseに対応できているという点は同じなのかもしれない(?)
通常のuseQueryとuseSuspenseQueryの大きな違いはdataがundefinedな状態を考慮する必要がなくなったことです。
だからuseSuspenseQuery使うならisPendingもisErrorも返り値として受け取る必要は無くなってdataだけ受け取れば良いっぽい。アツい

とはいえもはやReactの<Suspense></Suspense>をまったく使用していないのにSuspenseが効いてるのがなぜかわからず混乱してるのだが、そこから考えられることは...
pendingComponentが全部やってくれてる。最強。
ということ。
だがまぁそれだけという訳でもなく、useQuery/useSuspenseQueryを使っていることが前提という部分も大事か。
なぜなら普通にコンポーネント内でawaitするのは当然不可。クライアントコンポーネントはawait禁止なので
んでごり押しでuseEffect内でasync関数作ってその中でawaitしてそれを実行しようとしてもそれはSuspense機能しない。
サスペンスはエフェクトやイベントハンドラ内でデータフェッチが行われた場合にはそれを検出しません。
なのでtanstack queryみたいに隠蔽してくれるやつ使わないと無理だよね、という話はある
だがそれさえ使っちゃえばもう<Suspense>使わずとも勝手にSuspense機能してpendingComponentがfallbackとして表示されるようになる
それでいいのか?<Suspense>の存在意義めっちゃ薄くない?という混乱はあるが、とりあえずめっちゃ楽に書けるという事実は変わらないので助かる
ん-、まぁReactが将来を見越しての話なのかなー?という予想
tanstackとかのライブラリを全く使わずReact単体でもuse()
と<Suspense>だけ使えば非同期やらキャッシュやらを完全に対応できるようになるかもしれないという未来があり、そのReactオンリーという栄光のために残してある、ということなのかもしれない(?)

なんかこのpreloadとかもやりたいなぁというメモ
tanstack routerのloaderとuseSuspenseQueryを組み合わせるというもの
そうすれば既にuseSuspenseQuery発火の段階ではデータが揃ってるらしい、凄い

とりあえずtanstack query実装完了!
現状こんな感じ
TanStack Query編終了

さて、todos/パスとか本格的に作っていきたい
のでsupabaseでtable作ったりsupabase-jsをhonoの中で書いたりしていくか

ていうかjsxをhonoから返す予定は無いのでこのrenderer.tsxっていう規定のファイル不要かも?
import { jsxRenderer } from 'hono/jsx-renderer'
export const renderer = jsxRenderer(({ children }) => {
return (
<html>
<head>
{/* <link href="/static/style.css" rel="stylesheet" /> */}
</head>
<body>{children}</body>
</html>
)
})
index.tsxからミドルウェアとして呼び出してるっぽいんだけれども
import { renderer } from './renderer'
app.use(renderer)
まぁいいや

Supabase設定編
この辺り見つつ、supabaseのプロジェクト初期化、接続までやっていく
backend/に.env作成
前ハマったやつ。アクセスポリシー作らないとfetchしてもtableから空の配列しか返ってこないので注意
ここで設定できる
checkは無しで消して、usingの中をtrueにすれば良い
textかvarcharか...違いはあんま無さそうなのでとりあえずvarcharで
foreign keyも設定し、updateもdeleteもcascade(滝のように流れる=連動する=一心同体)で設定
todos.id -> users.idにした。userが消えたらtodoも全部消えるというロジックが理想なので、userが主従の主であるべき
毎回ちゃんとemail認証かけるの面倒だし1時間に3通までしか送れないのでとりあえずConfirm emailはオフにしとく
あとはsupabase.auth.signUp()
すればAuthentication -> usersに勝手に登録される

あとはsignUpして、そのユーザーからtodoを何個か登録して、select/create/update/delete確認すればとりあえずokかな
signup
const { data, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
})
fetch/insert/update/delete

-
getSession
はブラウザのローカルセッションを見るだけでサーバーに確認してる訳じゃないので改ざんされてる可能性を否定できないのでgetUser
の方が良いらしい
Since the unencoded session data is retrieved from the local storage medium, do not rely on it as a source of trusted data on the server. It could be tampered with by the sender. If you need verified, trustworthy user data, call getUser instead.
const { data, error } = await supabase.auth.getSession()
まぁクライアントサイドで高速にチェックしたい場合はgetSession
使えって感じらしいけど
On the client, you can instead use
getSession().session.user
for faster results.getSession
is insecure on the server.
-
getUser
はこれ。こっちはちゃんとサーバーに問い合わせてチェックする
const { data: { user } } = await supabase.auth.getUser()
ということでgetUser
使うか

いやでも使い分けすべきかも?
-
getUser()
でアカウント登録つまりsignupが済んでいるのか確認できる -
getSession()
でログイン中つまりsignin中が確認できる- ただ改ざんされる可能性はあるというのが面倒だな(今回は無視するか...)
いや違うな、どっちもセッション確認してログイン中かどうか見てるっぽいのでどっちも役割同じだ
アカウント作成済みかどうかみたいな情報に関してはそもそも不要だし不可能かもしれん
なぜなら、何も入力されてない状態でどうやってアカウント存在確認をするんだという話になるから
メアド打ってくれればそれを基にアカウント作成済みか確認できるが、セキュリティ的にそれは良くなさそうだし、何よりそんな手間をユーザーに求めるなら最初からユーザーに新規登録させてエラーで「既にアカウント登録済みです」って出すのと何ら変わらないのでそれで良くないかという感じがした
なので新規登録ボタンを出すべきかログインボタンを出すべきかというのを自動検出してどっちかだけを画面に出すみたいなことは不可能 & 不要だと判断したのでやらないことにする

envが読み取れない、初見のエラーだ
Uncaught Error: supabaseUrl is required.
-
VITE_
prefixはつけてる-
import.meta.env.○○
で読み取ろうとしてる
-
- .envの階層をbackend/でもルートディレクトリでも試した
- サーバー立てなおした
- .gitignoreに指定するのも指定しないのも試した
Hono使ってるからhono側のenv設定方法使わないといけないとか?

あーやっとわかった
frontend/直下に.env設置しないとだった。Viteが入ってるのはfront/のみなのでそのディレクトリ内に入れないといけないということなのだろう
ルートディレクトリでもbackend/でもダメだがfrontend/ならokというところは盲点だった
知見:Viteがインストールされているpackage.jsonの同階層に.envを置く!

追記
ここの認識がまだ甘かったのでめっちゃ痛い目を見た
んでbackendも同じようにvite入ってるよな・・?とは思う
supabase clientを使いたいファイルと同ディレクトリ内(backend/かfrontend/)の直下(もしくは親ディレクトリの直下でも良い?)に.env置かないとだめだった
なので兄弟ディレクトリ(?)の直下に置いちゃうと読み取れずにinternal server errorになって詰む

getUser
でtrueが取れれば認証は不要にするか
getSission
の方は自動でリフレッシュトークン発行するらしいけどgetUserの方は特に無さそうだが、これはつまり一度新規登録すると一生trueが返ってくるということだろうか
If the session has an expired access token, this method will use the refresh token to get a new session.
セッションに期限切れのアクセストークンがある場合、このメソッドは新しいセッションを取得するためにリフレッシュトークンを使用します。
だとするとややセキュリティ上良くなさそうではあるが(?)、まぁいい

signup, signin, signout のリクエストを実際に送る
- signup
- signin
- signout

ぐわー、またNavbarのコンポーネント内でuseEffect, useState使ってログイン状態をawaitで取得してsessionがあったらログアウトボタンを表示するという長いコードを書いてしまっている
ラグが視覚的に顕著だしコードも見づらいからできればuseEffect書きたくないんだよなぁ、わざわざ中にasync関数を作ってそれを実行するというのも面倒だし
んでuseEffect使うならuseStateもセットで使わざるを得なくなりどんどんコード量が増えていく
とにかくこれ良くない気がする
import { Link, useNavigate } from "@tanstack/react-router"
import { signOut } from "../auth/-functions/signOut"
import { isLoggedIn } from '~/backend/supabase/auth/isLoggedIn'
import { useEffect, useState } from "react"
export default function NavbarTop() {
const [session, setSession] = useState<boolean>(false)
const navigate = useNavigate()
useEffect(() => {
(async() => {
const _session = await isLoggedIn()
if (_session) {
setSession(_session)
}
})()
}, [])
const handleClick = async() => {
const ok = await signOut()
if (ok) {
navigate({ to: '/' })
}
}
return (
<div>
<Link to="/">ホーム</Link>
{session && <button onClick={handleClick}>ログアウト</button>}
</div>
)
}
useContextとか使ってグローバルにログイン状態保持するべきなんだろうけども、
現在のコンポーネント階層の設計上できないんだよなぁ
つまりNavbarの親でログイン状態を取得しておけばuseContextのProviderから子であるNavbarやらその他の子にcontextであるログイン状態を渡せるんだけど、Navbarの親は/
つまりRootであって、そこではログイン状態取得する必要無いので無駄だし遅くなるから避けたい
だがそうなるとNavbarが自分自身でawait isLoggedIn()
して取得するしかない、のでさっきのようなuseEffect & useStateコードを書かねばならない・・
とはいえ/
(Root)のbeforeLoadでawait isLoggedIn()
してその返り値を認証必要なページ全部に渡すような設計にするのが理想っぽい気はしている
なのでそうしたい、のだが...
beforeLoad:
のasync関数でreturnした値をcomponent:
に渡す方法がわからない
前挑戦したがダメだった。これは気力ある時にリベンジしたい
うーーーーーむ・・・とりあえず今回はhonoの勉強なのでここは飛ばす
(まぁ機能自体は実現できてるので問題は無い)

メモ
-
<Outlet />
を移動するなり中身改造するなりしても案外いけるのかもしれない(?)- 現在のコンポーネント階層を紙に図で書いてイメージしやすくすべきか
- 参考になりそうなサイト

ん?なんかsignIn()
は旧式ゆえ使えないっぽいぞ
あーsupabaseのv1のdocument見てたからっぽい、最新はv2なのか
v2だとこれだな。signInWithPassword
か

よしsignup, signin, signoutは実装できた
次はtodos/$userId
という動的path作ってそこで該当ユーザのtodoリストを表示するか
useParams()
でpath parameterを取得できそう

index.tsx内にコンポーネントも一緒に書いちゃうならRoute.useParams()
でいけるんだけど、コンポーネントは別ファイルに分離したいので、そうなるとRouteを参照することができない
だがuseParamsの引数from
を入れれば解決するっぽい
const { userId } = useParams({ from: '/todos/$userId/' })
よし、todos/$userId
でuserIdを取得できてることを確認した

ちなみに何となく"signin"ってわかり辛いよなぁという感があるのでユーザへの表示は"ログイン"にしてる
ユーザに表示されないコード部分はsigninにしてるが(supabaseの用意された関数名もsignIn
だし)

signin / signup 成功後のredirect pathも/todosではなく/todos/<userId>に変更せねば
なのでgetUserした後にuserのUUIDを取得する関数を作る必要あるな
多分getUserの返り値.data.user.id
でいけそうだ

新しい知見を得た
throw redirectする際にテンプレートリテラルは使えないっぽい
つまりこれはエラーになる
throw redirect({ to: `/todos/${userId}` })
エラーメッセージ
Type '`/todos/${any}`' is not assignable to type '"/todos/$userId" | "/" | "." | ".." | "/auth"'.ts(2322)
なのでこうしないといけない
throw redirect({
to: "/todos/$userId",
params: { userId }
})
こうするとエラーが消えた。へぇ~
claude 3.7
このエラーは、TypeScriptが /todos/${userId}
という動的に生成されたパス文字列を、TanStack Router の redirect
関数の to
プロパティに受け入れられる型と一致しないと判断しているためです。
この問題を解決するには、以下のいずれかの方法を試してみてください:
-
redirect
関数に型アサーションを使用する:
import { redirect } from "@tanstack/react-router"
import { getUser } from "~/backend/supabase/auth/getUser"
export const checkSession = async() => {
const user = await getUser()
const userId = user?.id
if (userId) {
throw redirect({
to: `/todos/${userId}` as const
})
}
}
- パスを作成する際にルートパラメータオブジェクトを使用する(推奨):
import { redirect } from "@tanstack/react-router"
import { getUser } from "~/backend/supabase/auth/getUser"
export const checkSession = async() => {
const user = await getUser()
const userId = user?.id
if (userId) {
throw redirect({
to: '/todos/$userId',
params: { userId }
})
}
}
TanStack Router では、パスパラメータを文字列内に直接埋め込むのではなく、params
オブジェクトを通じて渡すことが推奨されています。これにより型安全性が確保され、このようなエラーを防ぐことができます。
エラーメッセージを見る限り、あなたのルート定義では /todos/$userId
というパスが定義されていて、文字列テンプレートリテラルではなく、このような形式で指定する必要があります。

よし動的path(todos/$userId
)は対応できた
いよいよhono内にsupabase-jsで実際にORM的なコードを書いていくか
todos/$userId
から/api/todos/$userId
的なところ(?)にリクエスト送って該当ユーザのtodosを返却してもらってそれを表示したい
んで追加/削除/編集にも対応したい

今mockで/api
にリクエスト送ってそれを受け取るというのをtanstack query使って実装できてるのでそれを参考に書いていく
honoのディレクトリ構成もちゃんとやりたい
あとsupabaseのディレクトリは自分なりに考えて作っていくか

supabase-jsやreactのfetch方法を聞いたメモ
claude 3.7
reactのフロントエンドからhonoのAPIを通じてsupabaseからユーザー固有のTODOを取得するシステムを実装しましょう。以下に各部分のコード例を示します。
Honoでのバックエンドコード
import { Hono } from 'hono';
import { createClient } from '@supabase/supabase-js';
// Supabaseクライアントの初期化
const supabaseUrl = process.env.SUPABASE_URL || '';
const supabaseKey = process.env.SUPABASE_ANON_KEY || '';
const supabase = createClient(supabaseUrl, supabaseKey);
// HonoアプリのセットアップとCORSの設定
const app = new Hono();
// TODOを取得するエンドポイント
app.post('/api/todos', async (c) => {
try {
// リクエストボディからuserIdを取得
const { userId } = await c.req.json();
if (!userId) {
return c.json({ error: 'userId is required' }, 400);
}
// Supabaseからユーザー固有のTODOを取得
const { data, error } = await supabase
.from('todos')
.select('*')
.eq('user_id', userId);
if (error) {
console.error('Error fetching todos:', error);
return c.json({ error: error.message }, 500);
}
return c.json({ todos: data });
} catch (error) {
console.error('Unexpected error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 新しいTODOを作成するエンドポイント
app.post('/api/todos/create', async (c) => {
try {
const { title, userId } = await c.req.json();
if (!title || !userId) {
return c.json({ error: 'title and userId are required' }, 400);
}
const { data, error } = await supabase
.from('todos')
.insert([{ title, user_id: userId, completed: false }])
.select();
if (error) {
return c.json({ error: error.message }, 500);
}
return c.json({ todo: data[0] });
} catch (error) {
return c.json({ error: 'Internal server error' }, 500);
}
});
export default app;
Reactでのフロントエンドコード
import { useState, useEffect } from 'react';
interface Todo {
id: number;
title: string;
completed: boolean;
user_id: string;
}
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodoTitle, setNewTodoTitle] = useState('');
const [loading, setLoading] = useState(false);
// 実際のアプリケーションでは認証システムからユーザーIDを取得
const userId = 'user123'; // 例としてハードコード
// TODOを取得する関数
const fetchTodos = async () => {
setLoading(true);
try {
const response = await fetch('http://localhost:3000/api/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId }),
});
if (!response.ok) {
throw new Error('Failed to fetch todos');
}
const data = await response.json();
setTodos(data.todos);
} catch (error) {
console.error('Error fetching todos:', error);
} finally {
setLoading(false);
}
};
// 新しいTODOを作成する関数
const createTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTodoTitle.trim()) return;
try {
const response = await fetch('http://localhost:3000/api/todos/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: newTodoTitle,
userId,
}),
});
if (!response.ok) {
throw new Error('Failed to create todo');
}
const { todo } = await response.json();
setTodos([...todos, todo]);
setNewTodoTitle('');
} catch (error) {
console.error('Error creating todo:', error);
}
};
// コンポーネントがマウントされたらTODOを取得
useEffect(() => {
fetchTodos();
}, []);
return (
<div className="todo-app">
<h1>TODOリスト</h1>
<form onSubmit={createTodo}>
<input
type="text"
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
placeholder="新しいTODOを入力"
/>
<button type="submit">追加</button>
</form>
{loading ? (
<p>読み込み中...</p>
) : (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.title} - {todo.completed ? '完了' : '未完了'}
</li>
))}
</ul>
)}
</div>
);
}
export default TodoApp;
Supabaseのテーブル構造
Supabaseでは、以下のようなテーブル構造を想定しています:
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE,
user_id TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- ユーザーIDにインデックスを追加してクエリを高速化
CREATE INDEX todos_user_id_idx ON todos (user_id);
実装のポイント
-
Honoのバックエンド:
- POSTエンドポイントを使用してuserIdを受け取ります
- Supabase-jsを使って、そのuserIdに関連するTODOを取得します
- エラーハンドリングと適切なHTTPステータスコードを返します
-
Reactのフロントエンド:
- TODOデータのfetchと状態管理
- userIdをリクエストボディに含めてAPIにPOSTリクエスト
- 新しいTODOの作成機能も含んでいます
実際の実装では、認証システムと連携させてユーザーIDを動的に取得する必要があります。また、環境変数の設定や適切なCORS設定も忘れないようにしてください。
何か具体的な質問や機能追加があれば、お知らせください。

requestの受け取りはc.req.XXX
でいろいろできそうだ
今回はidをpath paramsにしているのでc.req.param
を使う感じかな
app.get('/api/todos/:id', async (c) => {
const id = c.req.param('id')
})
どうせ中でsupabase-jsをawaitで実行することになるからasyncは必須になるだろう

次にreactからのリクエスト方法を調べる
todos/$userId
のbeforeLoadでuserId取得してそれをリクエストとして送りたい
fetchのオプションでmethod, headers, bodyを設定したりするのかな
んで今回はそれをtanstack query使ってやる必要あるから難しくなりそうだ

beforeLoadでの返り値(userId)をcomponentに渡すことはできるのかという問題、普通にpropsとして自動で受け取れるようになってる...!?もしそうならめっちゃ助かるぞ
claude 3.7
はい、beforeLoad
の返り値を component
で受け取ることができます。これはTanStack Routerの機能の一つで、ルート間でデータを共有するのに便利な方法です。
beforeLoad
関数は、コンポーネントがレンダリングされる前に実行され、その返り値はルートのコンポーネントにプロップとして自動的に渡されます。例えば:
// checkSession.ts
export async function checkSession() {
// セッションチェックのロジック
const userData = await fetchUserData();
return { user: userData };
}
// Auth.tsx
function Auth(props) {
// props.user にアクセスできる (beforeLoadからの返り値)
const { user } = props;
return (
<div>
<h1>ようこそ、{user.name}さん</h1>
{/* その他のコンポーネントの内容 */}
</div>
);
}
あなたのコード例では、checkSession
関数の返り値は Auth
コンポーネントで利用できます。また、pendingComponent
として指定した AuthPending
コンポーネントは beforeLoad
の処理中に表示され、エラーが発生した場合は errorComponent
として指定した AuthError
コンポーネントが表示されます。
この仕組みを使って、認証状態やユーザーデータなどをルートロード時に取得し、それをコンポーネントに直接提供することができます。
試してみる

というかtodos/$userId
のpathでbeforeLoadするとき、userId自体はpathからそのまま取得できそうだな
んでそのuserIdを基にtodosデータ取得して、それを返却してcomponentにpropsとして渡す、という流れが妥当か

fetchこれ

ん、これもしかして、TanStack Queryでfetchをキャッシュするという行為が不要になるか?
Supabaseのクエリをsupabase-jsで書くわけだがこれは別にfetchでもないしなぁ
まぁでもPromiseを返すという点では同じ。どうすべきだろうか

ん?なんかいろいろミスってた
react側で全部supabase-jsのクエリ書いたりとかやろうとしてた、honoでクエリは実行せねば
流れ
react側のtanstack queryでhonoの/api/todos/:userId
をfetch
↓
honoでsupabase-jsのクエリ実行してその結果としてのtodosを返却
↓
それをreactが受け取り表示

なぜかCORSエラーが出だした。origin許可してるのに。なぜ??
(しかもずっといけてたのに急にエラーになった)

なんかfetch先のurlを相対パスにするか絶対パスにするかでエラーメッセージ変わるんだが、どっちでも良いんじゃなかったっけ?
const res = await fetch(`/api/todos/${userId}`) // これだとJSONではなくHTMLが返ってくるエラー
const res = await fetch(`http://localhost:5001/api/todos/${userId}`) // これだとCORSエラー
多分前者はエラー発生してサーバのデフォルトのindex.htmlのhtml返しちゃってるみたいなことだと思うので完全にfetchが機能してないような気はする
だから後者の絶対パスの方が正しくて、ただそこでCORS設定してないからエラーになっちゃってるだけ、て感じではないだろうか(?)
あーやっぱそうっぽい。初歩的なミスだが、ポートが変わるなら絶対パスにしないといけないらしい。相対パスにしないと同じポートを想定した上でリクエスト送っちゃうのか
gemini-2.0-pro-exp-02-05
fetch
の引数の URL は、絶対パスでも相対パスでも問題ありません。どちらを使用するかは、状況とコードの構成によって異なります。
絶対パス:
- 常に
http://
やhttps://
から始まる完全な URL です。 - どの場所から呼び出しても、常に同じリソースを指します。
- 異なるドメインやポート番号のリソースにアクセスする場合に必要です。
相対パス:
-
fetch
を呼び出す JavaScript ファイルの場所を基準とした URL です。 - 同じドメイン内の異なるパスにアクセスする場合に便利です。
-
./
(現在のディレクトリ)、../
(1つ上のディレクトリ) などを使用してパスを指定します。
http://localhost:5000/todos/$userId
から http://localhost:5001/api/todos/:userId
に fetch
する場合:
この場合は、異なるポート番号 (5000 と 5001) にアクセスするため、絶対パスを使用する必要があります。相対パスでは、ポート番号を変更することができません。
コード例 (絶対パス):
const userId = 123; // 例
fetch(`http://localhost:5001/api/todos/${userId}`)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
補足:
フロントエンドが http://localhost:5000
で動作している時、fetch('/api/todos/123')
のようなバックエンドのapiのルートからの相対パスで指定することも可能です。この時、ブラウザは自動的にhttp://localhost:5000/api/todos/123
へリクエストを送ります。
異なるポートやドメインにリクエストを送る場合は、明示的に絶対パスを指定する必要があります。
CORS(Cross-Origin Resource Sharing)に注意
異なるオリジン(プロトコル、ドメイン、ポートの組み合わせ)にリクエストを送信する場合、CORS の設定が必要になることがあります。サーバー側で適切な CORS ヘッダーを設定しないと、ブラウザがセキュリティ上の理由でリクエストをブロックすることがあります。
さて、そしたらやはりなぜCORSエラー出てるのかを特定せねば

あとrendererってやつ必要か?あとで調べたい
import { renderer } from './renderer'
app.use(renderer)

あー確かに、リクエストやレスポンスが実際にどんなヘッダーのせて送られてるかをchrome devtoolsのnetworkタブ(?)から見てみるか?
CORSエラーはフロントエンドで起きているように見えますが、実際にはサーバー側の設定が原因です。エラーに遭遇した場合は、サーバーのレスポンスヘッダーを確認しましょう。

めちゃくちゃ謎なのが、全オリジン・全パス許可してもだめなこと
app.use('*', cors({
origin: '*'
}))
tanstack queryを使ってた頃はできてたんだよな
今のやつはまだtanstack query使ってないただの直接fetchなのでそれが原因か?
最初tanstack query使って試した時は普通にできてたしなぁ
ということでさっきと同じようにtanstack queryを使ってfetchしてみる

ただそうなるとなぜ普通にfetchするのだとorigin許可してもエラーになるのかは気になるが・・

honoエンドポイントへのfetchと、
honoエンドポイントからsupabaseへのfetch
の2つともtanstack queryでキャッシングしないとだな

コンポーネントかカスタムフック内でしかuseParams使えないっぽいのでカスタムフック内でuseParams使ってuserId取得してそれをgetTodosの引数に渡してあげることにするか
なのでgetTodosは直接tanstack queryのqueryFnに渡すのではなく高階関数の返り値として使う
import { useSuspenseQuery } from "@tanstack/react-query"
import { todoKeys } from "../-key/key"
import { getTodos } from "./getTodos"
import { useParams } from "@tanstack/react-router"
export const useGetTodos = () => {
const { userId } = useParams({ from: '/todos/$userId/' })
const { data } = useSuspenseQuery({
queryKey: todoKeys.all,
queryFn: () => getTodos(userId),
})
return data
}

おそらくフロントは完成した(まだCORSエラーは出ている)
次はhono(バックエンド)内のsupabase-jsのfetchでもtanstack query使う

hono(バックエンド)でのfetchは、コンポーネントとか無いからサスペンスで囲むとかpendingComponentに設定するとかができないの大丈夫なんかな
クライアント側のSuspenseがそれ検知して勝手にサスペンドしてくれるのか?わからん
んでuseSuspenseQueryにすべきか?useQueryで良いか?
まぁ一応useSuspenseQueryにしとくか

ん、バックエンド側でもtanstack queryインストールしないと使えないな
ていうかコンポーネントでqueryProviderみたいなやつ初期設定しないといけなかったなぁ
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
うーん、honoでコンポーネントとか使えるのか・・?めんどくさそうだなぁどうしよう
普通にクライアント側のフォルダ内にuseQueryTodosみたいなファイル作っちゃってそれをバックエンドからインポートするだけっていうのはどうだろう、可能だろうか

できたけどエラーは変わらず。
キャッシュを見たところhonoからのqueryは実行すらされていない?でクライアントからのgetTodosしか走っておらずその途中でエラーが出たのか?
fetchingに3秒くらいなってずっとローディング中で、その後エラーになると同時にinactiveになってた
わからないのでとりあえずqueryなしの単純apiリクエスト/レスポンスを試してみる
(ユーザーテーブルに存在しないユーザーIDを指定しているのも良くないか?)

だめだ、/testというパスを作って普通にリクエスト送ってもCORSエラーだ
なぜ???

ん、honoの開発サーバ見てみたら重要そうなエラー出てるな、500エラー
あー、最初と違ってsupabase clientのファイルの場所をクライアント側ではなくバックエンド側に移してしまったかも?それによって読み取れなくなってエラー出てる?
いやでもフロント側で書こうとしたらsupabase-jsインストールしてないからエラーになる
じゃあ別にここはバックエンド側に最初からあったっぽいな
関係なさそう
デプロイしちゃえばVercelに環境変数設定できるので、これは最悪無視でも良さそうな気はする

そういえばfetchする関数内でtry catchでエラーハンドリングしてなかったからそこのエラーメッセージも見てみるか
うーんまぁ普通にFailed to fetch
だなぁ
エラーが出てもめげずに4回fetchしてるっぽく、4回目が失敗したらcatch内に書いといたthrow new Errorがやっと出るようになってる
普通に直接fetchしてみてもやっぱりだめだ、CORSエラー

なんで最初はできてたんだっけ?何が違う?
こんな単純なものすら受け取れなくなってしまった
app.get('/api/test', (c) => {
return c.text('テストだよー')
})
CORSでorigin許可するだけではだめで他に直接ヘッダー設定したりとか、リクエスト側でもfetchの際にオプション付けてヘッダー設定したりしないとダメとか?

geminiに聞く
gemini-2.5-pro-exp-03-25
CORSエラーが発生しているとのことですが、提示されたコードとエラーメッセージを見る限り、いくつかの原因が考えられます。
考えられる原因と対処法
-
ミドルウェアの適用順序:
Honoではミドルウェアは記述された順に適用されます。現在のコードでは、renderer
ミドルウェアがcors
ミドルウェアよりも先にグローバルに適用されています。// honoのindex.tsx // ... app.use(renderer) // ← これが先に適用される app.use('/api/*', cors({ // ← APIルートに対して後から適用される origin: 'http://localhost:5000', }) ) // ...
もし
renderer
ミドルウェアがリクエストを処理してレスポンスを返してしまったり、あるいは何らかの理由で処理を中断させたりすると、後続のcors
ミドルウェアが実行されず、CORSヘッダーが付与されない可能性があります。
特に、エラーメッセージに500 (Internal Server Error)
が含まれていることから、renderer
ミドルウェアか、あるいはルートハンドラ自体でエラーが発生し、CORSヘッダーが付与される前の段階で処理が失敗している可能性が高いです。対処法:
-
CORSミドルウェアを先に適用する: API関連のルートに対するCORS設定は、他のミドルウェア(特にレスポンスを返す可能性のある
renderer
など)より先に適用するのが一般的です。 -
renderer
ミドルウェアの適用範囲を見直す:renderer
がSSRやHTMLレンダリングのためのミドルウェアであれば、APIエンドポイント (/api/*
) には適用せず、HTMLを返す必要のあるルート(例:'*'
や'/'
など)にのみ適用するように変更します。
修正例:
// honoのindex.tsx (修正案) import { Hono } from 'hono' import { renderer } from './renderer' // renderer の内容を確認してください import { cors } from 'hono/cors' const app = new Hono() // 1. CORSミドルウェアをAPIルートに適用 // 可能な限り早い段階で、しかし対象パスを絞って適用するのが良いでしょう。 app.use('/api/*', async (c, next) => { console.log(`[${new Date().toISOString()}] Applying CORS for: ${c.req.path}`); // ログ追加 const handler = cors({ origin: 'http://localhost:5000', // 必要に応じて他のオプションも検討 // allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // allowHeaders: ['Content-Type', 'Authorization'], // credentials: true, // Cookie認証などを使う場合 }); await handler(c, next); // CORSミドルウェアを実行 // CORSミドルウェアがレスポンスヘッダーを追加したか確認(デバッグ用) // console.log('Response headers after CORS:', c.res.headers); }); // 2. APIルートの定義 app.get('/api/test', (c) => { console.log(`[${new Date().toISOString()}] Handling /api/test`); // ログ追加 try { // ここでエラーが発生していないか確認 return c.text('テストだよー from Hono API'); } catch (error) { console.error(`[${new Date().toISOString()}] Error in /api/test handler:`, error); // エラー時もレスポンスを返す return c.json({ message: 'Internal Server Error' }, 500); } }) // 他のAPIルートがあればここに追加 // 3. rendererミドルウェアを適用 // もしrendererがHTMLレンダリング用なら、APIルート(*)より後に、 // 具体的なパスや '*' で適用します。 // APIルートには不要なはずです。 // app.use(renderer) // ← グローバル適用をやめる app.get('*', renderer) // 例: API以外のすべてのGETリクエストに適用 export default app
-
CORSミドルウェアを先に適用する: API関連のルートに対するCORS設定は、他のミドルウェア(特にレスポンスを返す可能性のある
-
サーバーサイドでのエラー (500 Internal Server Error):
コンソールにGET http://localhost:5001/api/test net::ERR_FAILED 500 (Internal Server Error)
と表示されています。これは、CORSポリシーチェック以前に、サーバー側でリクエストの処理中にエラーが発生し、ステータスコード500でレスポンスが返された(あるいはレスポンス自体が壊れた)ことを示しています。
サーバーがエラーレスポンスを返す際、正常なレスポンスに必要なCORSヘッダー(Access-Control-Allow-Origin
など)が付与されないため、ブラウザはCORSエラーとして報告します。対処法:
- Honoサーバーを起動しているターミナル(コンソール)のログを確認し、
500 Internal Server Error
の具体的な原因を特定してください。スタックトレースなどが出力されているはずです。 - 上記の修正例のように、APIルートハンドラ内に
try...catch
を追加し、エラーハンドリングを強化します。 -
renderer
ミドルウェアの実装を確認し、意図せずAPIルートでエラーを引き起こしていないか確認してください。
- Honoサーバーを起動しているターミナル(コンソール)のログを確認し、
-
CORS設定の詳細:
単純なGETリクエストであればorigin
の指定だけで十分なことが多いですが、念のため他のオプションも確認します。-
メソッド:
GET
以外のメソッド(POST
,PUT
,DELETE
など)や、Content-Type
以外のヘッダー(Authorization
など)をリクエストで送信する場合、allowMethods
やallowHeaders
の設定が必要になることがあります。 -
Credentials: CookieやAuthorizationヘッダーを伴うリクエストの場合、サーバー側で
credentials: true
を設定し、クライアント側 (fetch) でもcredentials: 'include'
を指定する必要があります。
対処法:
- 必要に応じて
cors()
のオプションを追加します。(上記の修正例を参照)
-
メソッド:
デバッグ手順の推奨:
- サーバー側のログを確認: Honoサーバーのコンソール出力を最優先で確認し、500エラーの原因を特定・修正します。
-
ミドルウェアの順序変更: 上記の修正例のように、CORSミドルウェアを先に適用し、
renderer
の適用範囲を見直します。 -
シンプルなAPIエンドポイントでテスト: 問題を切り分けるために、他のミドルウェアの影響を受けない非常にシンプルなAPIエンドポイント(例:
app.get('/ping', (c) => c.text('pong'))
)を作成し、それにcors
ミドルウェアだけを適用して、クライアントからリクエストを送信してみます。これでCORSが通るか確認します。 -
curl
コマンドで確認: ターミナルから以下のコマンドを実行し、サーバーからの直接のレスポンスヘッダーを確認します。レスポンスヘッダーにcurl -v -H "Origin: http://localhost:5000" http://localhost:5001/api/test
Access-Control-Allow-Origin: http://localhost:5000
が含まれているか、またステータスコードが500になっていないか確認します。
まずはサーバーサイドの500エラーを解決することが、CORSエラー解消への近道だと思われます。Honoサーバーのログを確認してみてください。
なるほどなー
コンソールに GET http://localhost:5001/api/test net::ERR_FAILED 500 (Internal Server Error) と表示されています。これは、CORSポリシーチェック以前に、サーバー側でリクエストの処理中にエラーが発生し、ステータスコード500でレスポンスが返された(あるいはレスポンス自体が壊れた)ことを示しています。
サーバーがエラーレスポンスを返す際、正常なレスポンスに必要なCORSヘッダー(Access-Control-Allow-Origin など)が付与されないため、ブラウザはCORSエラーとして報告します。
リクエスト中にエラー起きて正常なレスポンス送れなくなったらCORSヘッダーを返せなくなるので、厳密にはCORSエラーではないのにCORSエラーになっちゃうってことか!
てことはさっきのinternal server errorは直さないといけないやつだったかも

あとこれは初耳だ、勉強になる
CookieやAuthorizationヘッダーを伴うリクエストの場合、サーバー側で credentials: true を設定し、クライアント側 (fetch) でも credentials: 'include' を指定する必要があります。

サーバーログのエラーのことを伝えたgeminiの返答
gemini-2.5-pro-exp-03-25
サーバーログありがとうございます!エラーの原因が明確になりましたね。
Internal server error: supabaseUrl is required.
at new SupabaseClient (...)
at createClient (...)
at C:\Users\rutob\Downloads\hono-todoapp\backend\supabase\client.ts:6:25
このログは、backend/supabase/client.ts
の中で Supabase クライアントを初期化しようとした際に、supabaseUrl
が渡されなかった(undefined
または空だった)ためにエラーが発生したことを示しています。これが原因でサーバーが 500 エラーを返し、結果としてブラウザ側では CORS ヘッダーがないというエラーに見えていたわけです。
根本的な原因:
Hono サーバー (バックエンド) のコード (backend/supabase/client.ts
) が、フロントエンド用のディレクトリ (frontend/.env.local
) に置かれた環境変数を読み込めていないためです。
-
import.meta.env
: これは Vite が提供する機能で、Vite の開発サーバーやビルドプロセスが.env
ファイルなどを読み込み、コード中のimport.meta.env.VITE_...
を実際の値に置き換えます。 -
Vite の環境変数のスコープ: Vite 開発サーバー (
vite
コマンド) は、デフォルトでは実行されたディレクトリ (今回の場合はbackend
ディレクトリ) を基準に.env
ファイルを探します。 -
今回の状況:
- Hono サーバーは
backend
ディレクトリで (おそらくvite
コマンドによって) 実行されている。 -
backend/supabase/client.ts
はimport.meta.env.VITE_SUPABASE_URL
を参照している。 - Vite サーバーは
backend
ディレクトリとその上位ディレクトリで.env
ファイルを探すが、frontend/.env.local
はその対象外。 - 結果として
import.meta.env.VITE_SUPABASE_URL
はundefined
になり、Supabase クライアントの初期化でエラーが発生。
- Hono サーバーは
解決策:
以下のいずれかの方法で、バックエンドの Hono サーバーが Supabase の環境変数を読み込めるように設定してください。
方法1: バックエンドディレクトリに .env
ファイルを置く (推奨度: 高)
-
backend
ディレクトリの直下に.env
という名前のファイルを作成します。(例:backend/.env
) -
そのファイルに、
frontend/.env.local
と同じ内容をコピーします。VITE_
プレフィックスはそのままにしておいてください (import.meta.env
を使っているため)。# backend/.env VITE_SUPABASE_URL=XXX VITE_SUPABASE_API_KEY=XXX
-
Hono/Vite サーバーを再起動します。
これで backend
で実行されている Vite サーバーが backend/.env
を読み込み、import.meta.env
に値が注入されるはずです。
方法2: Vite の設定で環境変数ファイルの場所を指定する
もし環境変数を frontend
ディレクトリで一元管理したい場合は、backend
の Vite 設定ファイル (backend/vite.config.ts
または backend/vite.config.js
) で envDir
オプションを指定します。
-
backend/vite.config.ts
(なければ作成) を編集します。 -
defineConfig
の中でenvDir
を設定します。// backend/vite.config.ts import { defineConfig } from 'vite' import honoPlugin from '@hono/vite-dev-server' // Hono用のプラグインなどがあればそのまま import path from 'path' export default defineConfig({ // Hono/Vite用のプラグイン設定などがあればここに plugins: [ honoPlugin({ entry: './src/index.tsx' // Honoのエントリーポイントに合わせてください }) // 他のプラグイン... ], // 環境変数を読み込むディレクトリを frontend ディレクトリに指定 envDir: path.resolve(__dirname, '../frontend'), // .envファイルで VITE_ 以外のプレフィックスも読み込みたい場合 (通常は不要) // envPrefix: 'VITE_', // デフォルトは 'VITE_' })
-
path.resolve(__dirname, '../frontend')
は、vite.config.ts
があるディレクトリ (backend
) から見て一つ上の階層のfrontend
ディレクトリを指します。 -
@hono/vite-dev-server
などのプラグインを使用している場合は、その設定も確認・記述してください。
-
-
Hono/Vite サーバーを再起動します。
注意点:
-
サーバーの再起動:
.env
ファイルの変更やvite.config.ts
の変更後は、必ず Hono/Vite サーバーを再起動してください。 -
.env
ファイルのコミット: Supabase の API キーなどの機密情報は Git リポジトリにコミットしないように、.gitignore
に.env
や.env.*
を追加しておきましょう。 -
as string
のリスク:import.meta.env.VITE_... as string
は、環境変数が未定義の場合にランタイムエラーを引き起こします(今回のように)。より安全にするなら、以下のように存在チェックを行うのがおすすめです。// backend/supabase/client.ts (改善案) import { createClient } from "@supabase/supabase-js"; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseApiKey = import.meta.env.VITE_SUPABASE_API_KEY; if (!supabaseUrl || !supabaseApiKey) { // サーバー起動時になるべく早くエラーを検知できるようにする throw new Error("Supabase URL and API Key must be defined in environment variables (VITE_SUPABASE_URL, VITE_SUPABASE_API_KEY)"); } // ここに来る時点で string であることが保証される (Viteが注入するため) export const supabase = createClient(supabaseUrl, supabaseApiKey);
まずは方法1を試して、Honoサーバーが正常に起動し、/api/test
エンドポイントが 500 エラーではなく正常なレスポンス(テストだよー
)を返すようになるか確認してみてください。それができれば、CORS の設定も正しく機能するはずです。
なるほどなぁ~~~
このログは、backend/supabase/client.ts の中で Supabase クライアントを初期化しようとした際に、supabaseUrl が渡されなかった(undefined または空だった)ためにエラーが発生したことを示しています。これが原因でサーバーが 500 エラーを返し、結果としてブラウザ側では CORS ヘッダーがないというエラーに見えていたわけです。
ということでbackend/ 直下にも.env.local作ったら無事直ったーーー!!!

もう不要だろと思ってフロントエンドの.env.local消したら画面が真っ白になった
フロントとバックの開発サーバのログには何もエラー出ないが、コンソールにはclientのsupabaseのエラーが出力されてる。これ↓
if (!supabaseUrl || !supabaseApiKey) {
// サーバー起動時になるべく早くエラーを検知できるようにする
throw new Error("VITE_SUPABASE_URLとVITE_SUPABASE_API_KEYが環境変数内に定義されてないとダメ")
}
んでどうやらフロントからバックのsupabase clientをインポートしてるのがまずいらしい
確かにbackend内には環境変数があるとは言え、インポート元であるfrontend側でそのsupabase clientを解決しようとするのかな、多分
そんで環境変数がfront内に見つからなくてエラー、みたいな流れだと思われる
まぁ確かにフロントで使いたいコードがバックに用意されてるのはディレクトリ設計おかしいだろというツッコミはありそうだ
確かに違和感もあるし

なのでそのbackendをimportしようとしてるfrontendのコードをbackendに移動しようとしたのだが、tanstack query使ってるからbackendでそれは使えないなぁ、、となった
backend側ではtsqueryをインストールしてないからなのもそうだが、前述の通りtanstack queryはインストールするだけでなくmain.tsx内で(コンポーネントで)セッティングする必要もあるので、それをhono側でやれるんか?みたいな話になる
できるのかもしれないけど、とりあえずバックエンドではtanstack query使わないで普通にfetchしちゃって良いとgeminiさんが言ってたのでとりあえず今回はそれでいく
gemini
おっしゃる通り、useQueryTodos
は React のフック (useSuspenseQuery
) を使っているので、そのまま Hono のバックエンド (backend/
) に移動することはできません。また、QueryClientProvider
は React アプリケーションに QueryClient のインスタンスを提供するためのコンポーネントなので、Hono サーバーサイドで直接使うものではありません。
ここで重要なのは、フロントエンドとバックエンドの役割分担を明確にすることです。
-
バックエンド (Hono):
- データベース (Supabase) と直接やり取りする。
- ビジネスロジックを実行する。
- フロントエンドからのリクエストに応じて、データを JSON などの形式で返す API エンドポイントを提供する。
-
フロントエンド (React + TanStack Query):
- UI を表示し、ユーザー操作を受け付ける。
- バックエンドが提供する API エンドポイントにリクエストを送信してデータを取得する。
- 取得したデータを TanStack Query を使ってキャッシュし、状態管理を行う (
useQuery
,useMutation
などを使用)。
現在の問題点:
frontend
の useQueryTodos
が、バックエンドのデータ取得関数 (backend/supabase/queryTodos
) を直接インポートして呼び出してしまっている点です。
正しいアプローチ (より良い方法):
-
バックエンド (Hono) に API エンドポイントを作成する:
-
backend/supabase/queryTodos.ts
は、Supabase からデータを取得するロジックだけを担当します (これは React や TanStack Query に依存しません)。 - Hono のルーター (
backend/src/index.tsx
) で、このqueryTodos
関数を使って特定のユーザーの Todo を返す API ルート (例:/api/todos/:userId
) を作成します。
-
-
フロントエンド (React) はその API エンドポイントを叩く:
-
frontend/src/.../useQueryTodos.ts
はフロントエンドに残します。 -
queryFn
の中で、backend/supabase/queryTodos
を直接呼び出す代わりに、先ほど Hono で作成した/api/todos/:userId
エンドポイントに対してfetch
リクエストを送信するように変更します。
-
具体的なコード修正例:
1. バックエンド (backend/src/index.tsx
) に API エンドポイントを追加:
// backend/src/index.tsx
import { Hono } from 'hono'
import { renderer } from './renderer' // 必要に応じて
import { cors } from 'hono/cors'
import { queryTodos } from '../supabase/queryTodos' // データ取得ロジックをインポート
const app = new Hono()
// CORS設定 (APIルート用) - これは以前の修正通り
app.use('/api/*', cors({
origin: 'http://localhost:5000', // フロントエンドのオリジン
// 必要なら他のオプションも
}));
// ★★★ 新しいAPIエンドポイントを追加 ★★★
app.get('/api/todos/:userId', async (c) => {
const userId = c.req.param('userId');
console.log(`[API] Fetching todos for userId: ${userId}`);
try {
// バックエンドのデータ取得関数を呼び出す
const todos = await queryTodos(userId);
// 取得したデータをJSONで返す
return c.json(todos);
} catch (error) {
console.error(`[API Error] Failed to fetch todos for ${userId}:`, error);
return c.json({ message: 'Failed to fetch todos', error: error.message }, 500);
}
});
app.get('/api/test', (c) => {
return c.text('テストだよー from Hono API');
});
// renderer はAPI以外のルートに適用 (必要に応じて)
app.get('*', renderer);
export default app
2. バックエンド (backend/supabase/queryTodos.ts
) の確認 (例):
(これは React や TanStack Query に依存しない形になっているはずです)
// backend/supabase/queryTodos.ts
import { supabase } from './client'; // Supabaseクライアント
// Todoの型定義 (例)
export interface Todo {
id: number;
user_id: string;
task: string;
is_completed: boolean;
created_at: string;
}
export const queryTodos = async (userId: string): Promise<Todo[]> => {
console.log(`[DB] Querying todos for userId: ${userId}`);
if (!userId) {
throw new Error('User ID is required');
}
// Supabaseからデータを取得するロジック
const { data, error } = await supabase
.from('todos') // 'todos' は実際のテーブル名に合わせてください
.select('*')
.eq('user_id', userId) // user_id カラムで絞り込み
.order('created_at', { ascending: true });
if (error) {
console.error('[DB Error] Supabase query failed:', error);
throw new Error(`Supabase query failed: ${error.message}`);
}
console.log(`[DB] Found ${data?.length ?? 0} todos for userId: ${userId}`);
return data || []; // データがない場合は空配列を返す
};
3. フロントエンド (frontend/src/routes/todos/$userId/-functions/useQueryTodos.ts
) の修正:
// frontend/src/routes/todos/$userId/-functions/useQueryTodos.ts
import { useSuspenseQuery } from "@tanstack/react-query";
import { todoKeys } from "../-key/key"; // Query Key
// import { queryTodos } from "~/backend/supabase/queryTodos"; // ← これは削除!
// ★★★ APIを叩くための新しい関数を作成 (別ファイルでもOK) ★★★
const fetchTodos = async (userId: string): Promise<Todo[]> => { // Todo型は適切に定義・インポート
console.log(`[Fetch] Fetching todos from API for userId: ${userId}`);
// Hono の API エンドポイントを叩く
const res = await fetch(`http://localhost:5001/api/todos/${userId}`); // ポート番号確認
if (!res.ok) {
const errorData = await res.json().catch(() => ({ message: 'Failed to parse error response' }));
console.error(`[Fetch Error] API request failed: ${res.status}`, errorData);
throw new Error(`API request failed: ${errorData.message || res.statusText}`);
}
// JSONレスポンスをパースして返す
const data: Todo[] = await res.json();
console.log(`[Fetch] Received ${data.length} todos from API`);
return data;
};
// ★★★ useQueryTodos フック ★★★
export const useQueryTodos = (userId: string) => {
const { data } = useSuspenseQuery({
queryKey: todoKeys.queryTodos(userId), // userId もキーに含めるのが一般的
// queryFn は API を fetch する関数に変更
queryFn: () => fetchTodos(userId)
});
// data の型が Todo[] になる (fetchTodos の返り値の型)
return data;
}
// Todo 型の定義 (例 - backend と共有するか、フロントで再定義)
interface Todo {
id: number;
user_id: string;
task: string;
is_completed: boolean;
created_at: string;
}
// Query Key の定義 (例 - key.ts ファイル)
// export const todoKeys = {
// all: ['todos'] as const,
// queryTodos: (userId: string) => [...todoKeys.all, userId] as const,
// }
回答まとめ:
-
Hono で
QueryClientProvider
は不要。 これは React フロントエンドのものです。 -
Hono で TanStack Query の React フック (
use...
) は使えない。 バックエンドは API を提供する役割。 -
より良い方法: バックエンド (Hono) は DB アクセスロジックとそれを公開する API エンドポイントを作成。フロントエンド (React + TanStack Query) はその API を
fetch
し、useQuery
などで状態管理・キャッシュを行う。 -
クライアントとバックエンド両方で使うべきか?:
-
クライアント (React): API フェッチとキャッシュのために
@tanstack/react-query
を 使うべき です。 -
バックエンド (Hono): 通常、TanStack Query は不要です。単純な
async/await
や Supabase クライアントで十分です。
-
クライアント (React): API フェッチとキャッシュのために
この構成にすることで、フロントエンドとバックエンドの関心事がきれいに分離され、それぞれの役割に集中できます。
これでサスペンスが効かないからuseSuspenseQuery使いたいやらキャッシュしたいやらというニーズが出てきたらまたその時方法を調べるとする

ということでディレクトリ整理しつつ書いていくか
backend側のtanstack query不要なので全部消しちゃって良かったりとかその辺り

なんかimportを補完からやるとこうなるけど、これだとエラーになる
import { queryTodos } from 'supabase/CRUD/queryTodos'
普通に相対パスでちゃんと指定してあげるとエラー解消できた
import { queryTodos } from '../supabase/CRUD/queryTodos'

よし、渡されたuserIdに一致するtodo一覧を取得する処理はできた
んで恐らく必要だと思われるのでfrontend/にもbackend/にも同じ.env.loval配置した
もう後は問題無くどんどん書いていけそうだ、ゴールが見えてきた

さて、とりあえず土台を書いた。こんな感じの構造にしたい
import { useParams } from "@tanstack/react-router"
import { useGetTodos } from "../-functions/useGetTodos"
import { TodoResponse } from "../-types"
import TodoList from "./TodoList"
import TodoForm from "./TodoForm"
export default function Todos() {
const { userId } = useParams({ from: '/todos/$userId/' })
const result: TodoResponse = useGetTodos(userId)
return (
<div>
<TodoList todos={result} />
<TodoForm />
</div>
)
}
import { ListProps } from "../-types";
export default function TodoList({ todos }: ListProps) {
if (typeof todos === 'string') {
return (
<h2>まだTodoはありません</h2>
)
}
return (
<ul>
{todos.map(todo => (
<li>{todo.id}: {todo.content}</li>
))}
</ul>
)
}
export default function TodoForm() {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// setStateをpropsとして受け取ってそれを使用して更新すべき?
// それともただ更新リクエスト送るだけでtanstack queryの特徴で勝手にTodoListも更新されるみたいなことある?
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
alert('投稿のリクエスト送るよ!')
}
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="新しいTodo" />
<button type="submit">登録</button>
</form>
)
}
ただ、TodoForm内で更新かけた時にその更新をTodoListが検知できないという問題
今まではfetch結果(response)をuseStateのstateとして保持してそれに対応するsetter関数を各コンポーネントにpropsとして渡すという方式で良かったのだが、今回はfetch結果としてのレスポンスをuseStateのstateとして保持できない。なぜならtanstack queryを今回使っており、その実行結果を普通の変数としてしか受け取れないから。
// こうしないといけない
const result: TodoResponse = useGetTodos(userId)
// こういう感じにしたいけど当然それはできない。つまりuseStateは使えない
const [result, setResult] = useState<TodoResponse>('')

あ、useMutation忘れてた。これ使いつつ、中でクエリ(キャッシュ?)を無効化すれば勝手に更新されるらしい
claude 3.7 sonnet
これはTanStack Queryを使ったReactアプリケーションにおけるよくある問題です。TodoFormでtodoを更新しても、TodoListが再レンダリングされない原因は、TodoFormとTodoListの間でQueryClientのキャッシュを共有できていないことにあります。
問題を解決するために、以下のアプローチが有効です:
- TodoFormでの更新後にクエリを無効化(invalidate)する
- QueryClientの設定でstaleTimeを調整する
- TodoFormコンポーネントにuserId情報を渡す
最も一般的な解決策は、TodoFormでのtodo操作後にクエリを無効化することです。以下のように実装を修正しましょう:
// TodoContainer.tsx
import { useParams } from "@tanstack/react-router"
import { useGetTodos } from "../-functions/useGetTodos"
import { TodoResponse } from "../-types"
import TodoList from "./TodoList"
import TodoForm from "./TodoForm"
export default function Todos() {
const { userId } = useParams({ from: '/todos/$userId/' })
const result: TodoResponse = useGetTodos(userId)
return (
<div>
<TodoList todos={result} />
<TodoForm userId={userId} />
</div>
)
}
// TodoForm.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createTodo /* or updateTodo */ } from '../-functions/api'
export default function TodoForm({ userId }) {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo) => createTodo(newTodo),
onSuccess: () => {
// 重要: キャッシュを無効化して再取得をトリガー
queryClient.invalidateQueries({ queryKey: ['todos', userId] })
// フォームのリセットなど他の処理
},
})
// フォーム処理と送信ハンドラ
// ...
return (
// フォーム要素
)
}
また、useGetTodos関数が以下のようにクエリキーを設定していることを確認してください:
// useGetTodos.ts
import { useQuery } from '@tanstack/react-query'
import { fetchTodos } from './api'
export function useGetTodos(userId: string) {
return useQuery({
queryKey: ['todos', userId],
queryFn: () => fetchTodos(userId),
}).data
}
このように実装することで、TodoFormでtodoを作成または更新した際に、同じクエリキー(['todos', userId])を持つクエリが無効化され、TodoListが自動的に最新データで再レンダリングされます。
最高だ・・やってみよう
こちらも参考にしつつ
まさにこれ
TODOアプリケーションでは基本的に入力フォームとTODO一覧が同一の画面にあります。その場合、新規TODOを追加して直ぐにTODO一覧の表示を更新したいです。しかし、デフォルトの挙動ではTODO追加後にデータを更新してくれません。上の検証環境ではデータ追加後、リロードをすると正しくデータが更新される挙動が確認できます。
そこで特定のクエリを指定して再取得しましょう。TanStack Queryにおけるキャッシュのinvalidate処理とはデータが古くなったことが明らかな場合、直接queryKeyを指定してデータを更新することを意味します。

ていうかkeyを["todos"]
にするんじゃなくて["todos", userId]
みたいにしないとか?
いやでもtodosのままで統一して良いかも
そのList全体を毎回更新するという形で良いかと思われるので

propsでuseStateのsetter関数を渡すなりしなくて良くなるの最高だな
ただinputのe.target.valueは捕捉し続けないといけないのでTodoFormコンポーネント内ではuseState使うのは依然必須か

fetchのoptionはこの通りにやってみよう
こんな感じ
import { UpdateTodoProps } from "../-types";
export const updateTodo = async({ user_id, content }: UpdateTodoProps): Promise<void> => {
const res = await fetch(`http://localhost:5001/api/todos/${user_id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id,
content
})
})
if (!res.ok) {
throw new Error('TODO新規投稿に失敗しました')
}
}
他の記事も見てみたけどoptionはこれで良さそう、とりあえず

とりあえずクライアント側のupdate系の処理はこれで完成したはず
あとはエンドポイント側でsupabase-js使ってokなはず

リクエストから受け取るbody(newTodoの中身)をhonoが取得する方法は多分これ
app.post('/entry', async (c) => {
const body = await c.req.json()
// ...
})

多分これでupdateは完成したのではないだろうか
動くか試してみる

ん、supabaseのpathをまたミスっていたので修正(この自動補完は本当になんなんだろう)

ダメだ、画面に何も起こらない
honoの開発サーバのログを見てみると、dbへのクエリでエラーが出てる
Error: DBへのクエリでエラーが発生しました:
at queryUpdateTodo (C:/Users/XXX/Downloads/hono-todoapp/backend/supabase/CRUD/queryUpdateTodo.ts:7:11)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
at async eval (C:/Users/XXX/Downloads/hono-todoapp/backend/src/index.tsx:36:19)
at async dispatch (file:///C:/Users/XXX/Downloads/hono-todoapp/backend/node_modules/hono/dist/compose.js:22:17)
at async cors2 (file:///C:/Users/XXX/Downloads/hono-todoapp/backend/node_modules/hono/dist/middleware/cors/index.js:74:5)
at async dispatch (file:///C:/Users/XXX/Downloads/hono-todoapp/backend/node_modules/hono/dist/compose.js:22:17)
at async file:///C:/Users/XXX/Downloads/hono-todoapp/backend/node_modules/hono/dist/hono-base.js:195:25
at async getRequestListener.overrideGlobalObjects (file:///C:/Users/XXX/Downloads/hono-todoapp/backend/node_modules/@hono/vite-dev-server/dist/dev-server.js:101:32)
at async responseViaResponseObject (file:///C:/Users/XXX/Downloads/hono-todoapp/backend/node_modules/@hono/node-server/dist/index.mjs:342:15)
うーん、ログの内容が薄すぎる、というかスタックトレースしか無いやん
わからん

仮説1
users.idとtodos.user_idはcacadeで紐づけているので、usersテーブルにまだ何も登録してないのにtodosテーブルに登録しようとしたことで不整合を検知しsupabaseが内部でエラーを投げたとか?
あまりにログの内容が薄すぎる(DBへのクエリで500エラーが起きたことしかわからない)からsupabaseの内部エラーとかその辺りの話なんじゃないかなぁという勝手な予想がある
なのでusersテーブルへの登録処理も書いた上で再度試してみるか

メモ
devtoolsのconsoleに出てたFailed to load resource
っていうエラーメッセージの意味とかも調べる必要はありそう?

signupとか全部フロント側でsupabaseクエリ叩いちゃってるからそこも新しくエンドポイント作ってhono側で対応しよう

あ、よく考えたら新規投稿ってuseMutationによる更新じゃないかも...?
いや大丈夫だった。CreateもuseMutationでokか
useMutationはCRUD操作のうちCreate/Update/Deleteの操作を扱います。
fetchのmethodオプションをPOST/PUT/DELETEなど区別すれば良いだけか

うわーイベントハンドラ内でtanstack query発火させたいんだけどどうすれば良いんだろう
イベントハンドラ内でfetch発火させたい
↓
use〇〇というtanstack queryを発火させたい
↓
だがuse〇〇はコンポーネットのトップレベルなどでしか使えない(普通の関数内では使えない)

claude曰く「useMutation使ってその返り値であるmutateをイベントハンドラ内で使っちゃえば良い」とのこと
んで更にその結果がtrueの場合のみもう1回getUserIdという別のfetchも飛ばしたいのだがそれはどうしようという悩み関して、「それはonSuccessでやっちゃえば良い」とのこと
なるほどなぁ
claude 3.7 sonnet
サインイン成功後にユーザーIDを取得し、そのIDを使ってnavigateする処理を実装しましょう。TanStack QueryとTanStack Routerを組み合わせた方法を示します。
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/router';
function SignInForm() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// サインイン用のmutation
const signInMutation = useMutation({
mutationFn: async (credentials) => {
const response = await fetch('/api/signin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials)
});
if (!response.ok) throw new Error('サインインに失敗しました');
return response.json();
},
onSuccess: async (data) => {
// サインイン成功時の処理
// ユーザー情報をキャッシュに設定
queryClient.setQueryData(['auth'], data);
try {
// ユーザーIDを取得するための追加リクエスト
const userResponse = await fetch('/api/me', {
headers: {
'Authorization': `Bearer ${data.token}` // 取得したトークンを使用
}
});
if (!userResponse.ok) throw new Error('ユーザー情報の取得に失敗しました');
const userData = await userResponse.json();
// ユーザーデータをキャッシュに設定
queryClient.setQueryData(['user'], userData);
// 成功アラート
alert('ログイン成功');
// 取得したユーザーIDを使ってナビゲーション
navigate({ to: `/todos/${userData.userId}` });
} catch (error) {
console.error('ユーザー情報取得エラー:', error);
// エラー処理
}
},
onError: (error) => {
// サインインエラー処理
console.error('サインインエラー:', error);
// エラーメッセージを表示するなどの処理
}
});
// サインインフォーム送信時のイベントハンドラ
const handleSubmit = (e) => {
e.preventDefault();
// mutationを実行してサインイン処理を開始
signInMutation.mutate({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">メールアドレス</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button
type="submit"
disabled={signInMutation.isPending}
>
{signInMutation.isPending ? 'サインイン中...' : 'サインイン'}
</button>
{signInMutation.isError && (
<div style={{ color: 'red' }}>
{signInMutation.error.message}
</div>
)}
</form>
);
}
このコードでは、以下のような流れで処理を行っています:
- サインインフォームを送信すると、
signInMutation.mutate()
が実行される - サインインに成功すると、
onSuccess
コールバックが呼ばれる -
onSuccess
内で、トークンを使ってユーザー情報を取得するための追加リクエストを送信 - ユーザー情報取得に成功したら、ユーザーIDを使って
/todos/${userData.userId}
へナビゲーション
このアプローチの利点:
- Reactのフックのルールに違反せず、イベントハンドラから間接的にデータフェッチを実行できる
- リクエストの順序が保証される(サインイン成功後にユーザー情報取得)
- エラーハンドリングが各段階で適切に処理される
必要に応じて、この処理をカスタムフックに抽出して再利用性を高めることもできます。

tanstack queryで覚えねばいけないことがたくさんあるなぁ
queryClient.setQueryDataとかkeyを関数にするか否かとか
キャッシュ自体は一意の定数つけとけば良さそうなんだけど、そのキャッシュの定義時には何も設定していなかった「そのキャッシュの中身そのもの」みたいな概念があるっぽく(まぁそりゃあそうか)、それをセットするのがsetQueryDataの第二引数だと思われる
setQueryData(queryKey, newData)
setQueryData(queryKey, (oldData) => newData)

signIn実装できた気がするので試してみる
ログイン成功のalertが出てtodos/:userIdのページまで無事に飛ぶのだが、ログアウトボタンが出てこないしホームに戻るとログインやり直しになる。
やはり、なぜかlocalStorageにcookieが保存されてない
あとキャッシュがStaleではなく即Inactiveになっているのも変(todos
キャッシュはStaleになるのに)

これ関係ありそう?アンチパターンを踏んだかも
(v4の説明ではあるので古い?けど)
onSuccess is no longer called from setQueryData
This was confusing to many and also created infinite loops if setQueryData was called from within onSuccess. It was also a frequent source of error when combined with staleTime, because if data was read from the cache only, onSuccess was not called.
Similar to onError and onSettled, the onSuccess callback is now tied to a request being made. No request -> no callback.
If you want to listen to changes of the data field, you can best do this with a useEffect, where data is part of the dependency Array. Since React Query ensures stable data through structural sharing, the effect will not execute with every background refetch, but only if something within data has changed:
const { data } = useQuery({ queryKey, queryFn }) React.useEffect(() => mySideEffectHere(data), [data])
和訳
onSuccessはもはやsetQueryDataから呼び出されない
これは多くの人を混乱させ、またsetQueryDataがonSuccessの中から呼び出された場合、無限ループを発生させた。また、データがキャッシュからのみ読み込まれる場合、onSuccessは呼び出されないため、staleTimeと組み合わせるとエラーの原因となることが多かった。
onErrorやonSettledと同様に、onSuccessコールバックはリクエストが行われたことに結びつきます。リクエストなし→コールバックなし。
dataフィールドの変更をリッスンしたい場合は、dataが依存関係のArrayの一部であるuseEffectを使用するのが最適です。React Queryは構造共有によって安定したデータを保証するので、エフェクトはバックグラウンドのリフェッチごとに実行されるのではなく、データ内の何かが変更された場合にのみ実行されます:
あーそういうことか
そもそもsuccessとは何の"成功"なのかということを考えれば確かにそうか
リクエストつまりfetchが実際に行われた場合、その成功時のみしかonSuccessは発火しない。
つまり既にリクエスト(fetch)が終わって2回目以降にそのリソースにアクセスしようとしてキャッシュから読み取るだけみたいになってる状態だと、"成功"も何もないのでonSuccessは発火しない
...ということに注意せよ、という話
もしそれやりたいならuseEffetct使え、かぁ・・

ていうかこの1文って、
onSuccess is no longer called from setQueryData
以下の間違えでは?という気がするなぁ
setQueryData is no longer called from onSuccess

いや、関係ないか、よく考えたら
どちらにせよ今回は初めてのリクエストだからリクエストはブラウザのキャッシュで留まっているのではなく元サーバーまで絶対に送られてる。だからonSuccessはちゃんと呼ばれてる
(というかonSuccess内のalertが出るんだからそりゃあ呼ばれてるか・・・忘れてた)

メモ:ログイン成功のalertが出るまで5秒くらいかかってるので遅すぎるし、それにFallbackがついてないのがやばい

まず現状整理
- localStorageにセッションが保存されてない
- キャッシュは2つ作られてはいるんだけど即Inactiveに入っている
1に関してはhono側で呼び出しているsupabase.auth.signInWithPassword
の部分が機能していないのが原因のはず
2に関してはqueryClient.setQueryDataのやり方がおかしいのかもしれない
1から見ていくか

2に関してのメモ:
すぐinactiveになるやつ、これか?
gcTimeが0にされてる?んでそれゆえ速攻ガベージコレクションされてinactiveになっているのかも
でもやはりそんなはずはない気はするけど
だからこそデフォルトのgcTimeは5minに設定されています。

honoの以下signInエンドポイントにおいてsuccessがちゃんとtrueになって中に入っているのは確認できた
app.post('/api/signin', async (c) => {
const { mailAddress, password } = await c.req.json()
const success = await signIn({ mailAddress, password })
if (success) {
return c.json({
status: 'success',
message: 'API: signInに成功しました',
mailAddress
})
} else {
return c.json({
status: 'failed',
message: 'API: singInに失敗しました',
mailAddress
})
}
})
なのでsignInのfetchリクエストはちゃんと送ってるしそこからちゃんとレスポンスも返してる

userId取得のリクエストもちゃんと成功してることを確認
(遷移先のpathを見ればわかることだが、当然userIdもしっかり取れてる)
app.get('/api/get/userId', async (c) => {
const userId = await getUserId()
if (typeof userId === 'string') {
return c.json({
status: 'success',
userId
})
} else {
return c.json({
status: 'failed',
message: 'API: userId取得に失敗'
})
}
})

知見
chrome devtoolsのnetworkタブでダウンロードされたjsを眺めていたら、HTTPレスポンスステータスコードが304になっているのばかりだった
304の意味を調べたら、「リクエストされた内容はNot Modified(修正されていない)ので再レスポンスする必要はないからキャッシュをそのまま使ってokやで」という意味らしい
今回tanstack queryでキャッシュしているからだろう

一応これ読んどく

honoのindex.tsx内で呼び出してるsignIn.tsを確認したがちゃんと呼ばれてた
import { supabase } from "~/backend/supabase/client"
import { SignInProps } from "~/backend/supabase/types"
export const signIn = async({ mailAddress, password }: SignInProps): Promise<boolean> => {
const { data, error } = await supabase.auth.signInWithPassword({
email: mailAddress,
password
})
if (!data || error) {
console.error('signIn.tsでエラーだよ')
console.error(`data: ${data}`)
console.error(`error: ${error}`)
return false
}
console.log('成功したで')
console.log('dataは', data)
console.log('emailは', mailAddress)
console.log('passwordは', password)
return true
}
つまり現状は、signInWithPasswordというlocalStorageを利用するはずのsupabaseのメソッドが実行されているのに、localStorageには何も保存されていないという状況
なぜや・・・どういうことや

supabase.auth.signInWithPassword
自体のソースコードを見に行ってみるか
/**
* Log in an existing user with an email and password or phone and password.
*
* Be aware that you may get back an error message that will not distinguish
* between the cases where the account does not exist or that the
* email/phone and password combination is wrong or that the account can only
* be accessed via social login.
*/
async signInWithPassword(
credentials: SignInWithPasswordCredentials
): Promise<AuthTokenResponsePassword> {
try {
let res: AuthResponsePassword
if ('email' in credentials) {
const { email, password, options } = credentials
res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
headers: this.headers,
body: {
email,
password,
gotrue_meta_security: { captcha_token: options?.captchaToken },
},
xform: _sessionResponsePassword,
})
} else if ('phone' in credentials) {
const { phone, password, options } = credentials
res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
headers: this.headers,
body: {
phone,
password,
gotrue_meta_security: { captcha_token: options?.captchaToken },
},
xform: _sessionResponsePassword,
})
} else {
throw new AuthInvalidCredentialsError(
'You must provide either an email or phone number and a password'
)
}
const { data, error } = res
if (error) {
return { data: { user: null, session: null }, error }
} else if (!data || !data.session || !data.user) {
return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
}
if (data.session) {
await this._saveSession(data.session)
await this._notifyAllSubscribers('SIGNED_IN', data.session)
}
return {
data: {
user: data.user,
session: data.session,
...(data.weak_password ? { weakPassword: data.weak_password } : null),
},
error,
}
} catch (error) {
if (isAuthError(error)) {
return { data: { user: null, session: null }, error }
}
throw error
}
}

!!!ログ出してみようと思ったら、そもそもこの関数自体が実行されてないっぽいぞ
async signInWithPassword(
credentials: SignInWithPasswordCredentials
): Promise<AuthTokenResponsePassword> {
console.log('あいうえおあいうえお') // これが出ない!
try {
...
いや奇妙すぎるな
signInWithPasswordの実行結果(返り値)としてのdataは取得できていることがsingIn.tsで既に確認済みだが、signInWithPasswordは実行されていないのではないかという仮説が発生してしまった
まじでなんだこれ

ソースコードはこれ

この謎すぎる現象をgeminiに聞いてみた
Gemini 2.5 Pro Preview 03-25
問題の分析
ご提示のコードと状況説明を拝見しました。混乱されている点は非常によく理解できます。問題の核心は、Supabaseの認証処理をどこで実行しているか、そしてそれに伴うセッション情報の保存場所にあります。
-
signInWithPassword
の実行場所:- あなたのコードでは、Reactクライアント (
SignIn.tsx
->useSignIn.ts
->fetchSignIn.ts
) がHono APIエンドポイント (/api/signin
) を呼び出しています。 - そして、Hono APIエンドポイント内 (
index.tsx
->signIn.ts
) でsupabase.auth.signInWithPassword
が実行されています。 - つまり、
signInWithPassword
はHonoのサーバーサイドで実行されています。
- あなたのコードでは、Reactクライアント (
-
console.log
が出ない理由:-
vscode
からnode_modules
内のGoTrueClient.ts
を直接編集しても、それがHonoサーバーの実行プロセスに反映されていない可能性が高いです。 - Hono (または内部で使用しているVite/Node) は、ビルド時や起動時に
node_modules
から必要なコードを読み込みます。実行中に直接ファイルを編集しても、すでにメモリにロードされたコードやビルド済みのコードが使われるため、編集内容が反映されないことが多いです。 -
signIn.ts
でdata
が取得できていることから、supabase.auth.signInWithPassword
関数自体は 間違いなく実行されています。ログが出ないのは、編集したファイルが実行時に使われていないか、ログ出力の設定の問題と考えられます。
-
-
LocalStorageに何も保存されない理由:
- これが最も重要な点です。Supabaseのクライアントライブラリ (
supabase-js
) は、デフォルトではブラウザ環境で実行されたときに、認証情報を自動的にブラウザのLocalStorageに保存します。 - あなたのコードでは、
signInWithPassword
は サーバーサイド (Hono) で実行されています。サーバーサイドのJavaScriptプロセスには、ブラウザのLocalStorageという概念は存在しません。 - Hono API (
/api/signin
) は、Supabaseでの認証が成功したかどうかを示す{ status: 'success' }
というJSONを返しているだけで、Supabaseから返された実際のセッション情報(アクセストークン、リフレッシュトークンなど)をクライアントに返していません。 - そのため、クライアントサイド(React)のSupabaseインスタンスは認証されたことを知らず、LocalStorageにも何も書き込まれません。
- これが最も重要な点です。Supabaseのクライアントライブラリ (
-
「ログイン成功」アラートが表示される理由:
- クライアントサイドの
useSignIn
フックは、Hono API (/api/signin
) から{ status: 'success' }
というレスポンスを受け取ると、onSuccess
コールバックを実行します。 - この
onSuccess
内でalert('ログイン成功')
を呼び出しているため、アラートが表示されます。これはあくまで API呼び出しが成功したこと を示しているだけであり、クライアントサイドでSupabaseの認証セッションが確立されたこと を意味するわけではありません。 -
fetchUserId
が成功しているように見えるのも、おそらくサーバーサイドの/api/get/userId
エンドポイントを叩いており、そのエンドポイント内のgetUserId
関数(これもサーバーサイドで実行される)が、直前の/api/signin
で認証されたサーバーサイドのSupabaseクライアントのコンテキストからユーザーIDを取得できているためだと考えられます。(getUserId
の実装によります)
- クライアントサイドの
矛盾の解消
-
signInWithPassword
はサーバーサイドで 実行されています (signIn.ts
でdata
が取れているため)。 -
console.log
が出ないのは、編集したファイルが実行時に反映されていないか、ログ設定の問題です。 - LocalStorageに保存されないのは、認証がサーバーサイドで行われ、セッション情報がクライアントに渡されていない ためです。
解決策:クライアントサイド認証への変更(推奨)
Supabaseの認証を利用する場合、特別な理由がない限り、クライアントサイドで直接認証処理を行うのが最も一般的で簡単な方法です。これにより、Supabaseライブラリが自動的にセッション管理(LocalStorageへの保存、トークンの更新など)を行ってくれます。
以下のように修正することをお勧めします。
-
Hono API (
/api/signin
) を削除または変更:- クライアントサイドで認証を行うため、このエンドポイントは基本的に不要になります。
-
signIn.ts
とfetchSignIn.ts
を削除:- これらも不要になります。
-
クライアントサイドにSupabaseクライアントを用意:
- Reactアプリケーションのエントリーポイントなどで、クライアントサイド用のSupabaseクライアントインスタンスを作成します。環境変数 (
NEXT_PUBLIC_
やVITE_
プレフィックス推奨) を使ってURLとAnon Keyを設定します。
// 例: src/lib/supabaseClient.ts (クライアント用) import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL // Viteの場合 // const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL // Next.jsの場合 const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY // const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY if (!supabaseUrl || !supabaseAnonKey) { throw new Error("Supabase URL or Anon Key is missing in environment variables."); } export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { // storage: localStorage, // デフォルトでLocalStorageが使われる autoRefreshToken: true, persistSession: true, // セッションを永続化する (デフォルトtrue) detectSessionInUrl: true, // URLからのセッション検出 (メール認証などに必要) }, })
- Reactアプリケーションのエントリーポイントなどで、クライアントサイド用のSupabaseクライアントインスタンスを作成します。環境変数 (
-
useSignIn.ts
を修正:-
fetchSignIn
の代わりに、クライアントサイドのsupabase
インスタンスを使って直接signInWithPassword
を呼び出します。 -
fetchUserId
の代わりに、認証成功後にsupabase.auth.getUser()
を使用するか、signInWithPassword
の戻り値からユーザー情報を取得します。
// useSignIn.ts の修正案 import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import { authKeys } from "../-key/key"; import { AuthProps } from "../-types"; import { supabase } from "~/lib/supabaseClient"; // ★ クライアントサイドのSupabaseクライアントをインポート export const useSignIn = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); const { mutate } = useMutation({ mutationFn: async (credentials: AuthProps) => { const { mailAddress, password } = credentials; // ★ クライアントサイドで直接 Supabase の signInWithPassword を呼び出す const { data, error } = await supabase.auth.signInWithPassword({ email: mailAddress, password: password, }); if (error) { console.error("Supabase Sign In Error:", error); // エラーメッセージをスローして onError で捕捉できるようにする throw new Error(error.message || "サインインに失敗しました。"); } if (!data?.user) { // 通常は error が発生するはずだが念のため throw new Error("ユーザー情報の取得に失敗しました。"); } // 成功したら data (user, session を含む) を返す return data; }, onSuccess: (data) => { // ★ data に { user, session } が含まれる const userId = data.user.id; // ★ Supabaseライブラリが自動でLocalStorageにセッションを保存するはず // React Query のキャッシュを更新 (任意) // 必要であれば user や session 全体をキャッシュしても良い // queryClient.setQueryData(authKeys.signIn, data); queryClient.setQueryData(authKeys.userId, userId); // ユーザーIDをキャッシュ console.log("クライアントサイド認証成功:", data); // デバッグ用 alert("ログイン成功"); // アラート表示 // ログイン後のページへ遷移 navigate({ to: `/todos/${userId}` }); }, onError: (error: Error) => { // エラー処理 console.error("Sign In Mutation Error:", error); alert(`ログイン失敗: ${error.message}`); } }); // mutate関数を返す (元の型定義に合わせる) return mutate as (credentials: AuthProps) => void; };
-
-
Hono API (
/api/todos/:userId
) の認証:- クライアントサイドで認証が完了すると、クライアントサイドの
supabase
インスタンスを使ってAPIを呼び出す際に、自動的にAuthorization: Bearer <supabase-jwt>
ヘッダーが付与されるようになります(fetch
を直接使う場合は手動で設定が必要な場合あり)。 - Hono側では、リクエストヘッダーからJWTを取得し、Supabase Adminクライアントなどを使ってそのJWTを検証し、ユーザーを特定する必要があります。これには
@supabase/supabase-js
をサーバーサイドで使うか、あるいはHonoのJWTミドルウェアなどを利用します。
// Hono API (index.tsx) での認証例 (要 Supabase Admin Client または適切なJWT検証) import { createClient } from '@supabase/supabase-js'; // サーバー用クライアント // サーバーサイド用のSupabaseクライアント初期化 (サービスキーを使用) const supabaseAdmin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!); // ... 他のimport // 認証ミドルウェア (例) app.use('/api/todos/*', async (c, next) => { const authHeader = c.req.header('Authorization'); const token = authHeader?.replace('Bearer ', ''); if (!token) { return c.json({ status: 'failed', message: '認証トークンがありません' }, 401); } // トークンを検証してユーザー情報を取得 const { data: { user }, error } = await supabaseAdmin.auth.getUser(token); if (error || !user) { return c.json({ status: 'failed', message: '無効なトークンまたはユーザーが存在しません' }, 401); } // ユーザー情報をコンテキストにセットして後続処理で使えるようにする c.set('user', user); await next(); }); // Todo取得エンドポイント (認証ミドルウェアの後) app.get('/api/todos/:userId', async (c) => { const user = c.get('user'); // ミドルウェアでセットされたユーザー情報 const requestedUserId = c.req.param('userId'); // ★ ログインユーザー自身のTodoのみ取得できるようにチェック if (user.id !== requestedUserId) { return c.json({ status: 'failed', message: 'アクセス権がありません' }, 403); } // userIdを使ってTodoを取得 (認証済みなので安全) const result = await queryTodos(user.id); // ... (以降の処理は同じ) }); // userIdパラメータが不要になる可能性もある app.get('/api/todos', async (c) => { // /:userId を削除 const user = c.get('user'); if (!user) { //念のためチェック return c.json({ status: 'failed', message: '認証が必要です'}, 401); } const result = await queryTodos(user.id); // 認証されたユーザーのIDを使用 if (result === null || result.length === 0) { // resultが配列と仮定 return c.json({ status: 'success', data: [], message: 'まだTodoはありません' }); } else { return c.json({ status: 'success', data: result, message: 'Todo取得完了' }); } }); // ... 他のエンドポイント
- クライアントサイドで認証が完了すると、クライアントサイドの
このクライアントサイド認証のアプローチにより、Supabaseの標準的な認証フローに乗り、LocalStorageへのセッション保存も期待通りに機能するはずです。
なるほど!!そういうことか
単にブラウザのlocalstorageいじろうとするjsコードをサーバ側(hono側)で実行しても意味無いに決まっとるやん、クライアント側で実行せなあかんで、ということか
んでソースコードごと編集したのにログが出ない理由は、honoが既にロードしたメモリからしか読み取らないようにしてるからとかいろいろ理由がありそう。だから編集しても意味無い場合が結構ありそうだなぁーという注意は今後していきたい。知見だ
- Hono (または内部で使用しているVite/Node) は、ビルド時や起動時に node_modules から必要なコードを読み込みます。実行中に直接ファイルを編集しても、すでにメモリにロードされたコードやビルド済みのコードが使われるため、編集内容が反映されないことが多いです。
- signIn.ts で data が取得できていることから、supabase.auth.signInWithPassword 関数自体は 間違いなく実行されています。ログが出ないのは、編集したファイルが実行時に使われていないか、ログ出力の設定の問題と考えられます。
じゃあhonoってどういう時に使うんだ?存在意義がわからない
gemii
おっしゃる通り、React、Tanstack Query、Tanstack Router、そしてSupabaseクライアントライブラリを組み合わせれば、クライアントサイドだけで非常に多くの機能を持つアプリケーションを構築できます。特にSupabaseは認証、DBアクセス、リアルタイム機能などをクライアントから直接扱えるように設計されているため、「バックエンドAPIサーバーは不要なのでは?」と感じるのは自然な流れです。
では、なぜHonoのようなバックエンドフレームワークが存在し、多くの開発者に利用されているのでしょうか?その理由は、クライアントサイドだけでは解決が難しい、あるいは不可能な課題や要件が存在するからです。
Hono (バックエンドフレームワーク) が必要になる主な理由:
-
セキュリティ:
-
秘密情報の保護: APIキー(例: Stripe、SendGrid、Twilioなど外部サービス)、Supabaseの
service_role
キー(RLSをバイパスできる強力なキー)など、絶対にクライアントサイドに公開してはいけない情報を扱う処理は、バックエンドで行う必要があります。Honoサーバーならこれらのキーを安全に保管し、必要な処理を実行できます。 - 複雑な認可ロジック: SupabaseのRow Level Security (RLS) は強力ですが、それだけでは表現しきれない複雑なアクセス制御やビジネスルールが必要な場合があります。「特定の条件下でのみ更新を許可する」「複数のテーブル情報を参照してアクセス権を判断する」といったロジックは、バックエンドで実装する方が安全かつ管理しやすいことがあります。
- 不正アクセスの防御: レートリミット(API呼び出し回数制限)、IPアドレス制限、より高度なボット対策などをバックエンドで実装することで、アプリケーション全体のセキュリティを強化できます。
-
秘密情報の保護: APIキー(例: Stripe、SendGrid、Twilioなど外部サービス)、Supabaseの
-
ビジネスロジックのカプセル化:
- 複雑なデータ操作: 複数のテーブルを結合・集計したり、外部APIから取得したデータと組み合わせたり、重い計算処理を行ったりする場合、それらをバックエンドAPIとしてまとめておくと、クライアント側のコードがシンプルになり、処理も効率化できます。
- 一貫性の担保: 複数のステップにわたる処理(例: 注文処理で在庫確認→決済→DB更新→メール送信)をトランザクションのように扱いたい場合、バックエンドで一連の流れを管理する方が確実です。
- 再利用性: 複数のクライアント(Web、モバイルアプリ、他のサービス)から利用される共通のロジックは、バックエンドAPIとして提供するのが効率的です。
-
外部サービス連携:
- メール送信、プッシュ通知、ファイル変換、支払い処理など、多くの外部サービス連携は、セキュリティ(APIキー管理)や安定性の観点からバックエンドを介して行われます。
-
パフォーマンス最適化:
- データ整形・集約: クライアントが必要とする形式にデータを加工・集約してから返すことで、クライアントに送信するデータ量を削減し、ネットワーク負荷やクライアント側の処理負荷を軽減できます。
- キャッシング: よくアクセスされるデータや計算結果をバックエンドでキャッシュすることで、レスポンス速度を向上させ、データベースへの負荷を減らせます。
-
サーバーサイドレンダリング (SSR) / エッジコンピューティング:
- Honoは特にCloudflare Workersのようなエッジ環境で高速に動作するように設計されています。エッジでAPIを提供することで、ユーザーに近い場所で処理を行い、低遅延を実現できます。また、フレームワークによってはSSR(サーバーサイドレンダリング)のためにバックエンド実行環境が必要です。(Hono自体はAPIフレームワークですが、エッジでの活用文脈があります)
Supabase と Hono の関係性:
- Supabaseは強力なBaaSであり、多くのバックエンド機能を肩代わりしてくれます。これにより、開発者はより少ないコードでアプリケーションを構築できます。
- しかし、上記のようなSupabaseだけではカバーしきれない、あるいはバックエンドで処理した方が適切な要件が出てきた場合に、Honoのようなフレームワークが役立ちます。
- Hono API内でSupabaseのAdminクライアント(
service_role
キーを使用)を使い、RLSをバイパスして管理操作を行ったり、複雑なクエリを実行したりできます。
結論:
クライアントサイド完結型のアーキテクチャは、多くのシンプルなアプリケーションやプロトタイピングにおいて非常に有効です。しかし、アプリケーションが成長し、より複雑な要件(高度なセキュリティ、複雑なビジネスロジック、外部連携、パフォーマンス最適化など)が出てくると、バックエンドを持つことの利点が大きくなります。
Honoは、そのような要求に応えるための、軽量で高速、かつ柔軟な選択肢の一つとして存在意義があります。特にAPIサーバーを構築したい場合や、エッジコンピューティングを活用したい場合に強力なツールとなります。
したがって、「Honoが不要」なのではなく、「アプリケーションの要件に応じて、クライアントサイドのみで十分か、バックエンドが必要か」を判断し、必要な場合にHonoのようなフレームワークを活用する、と考えるのが適切でしょう。今回のサインイン処理に関しては、クライアントサイドで完結させる方がシンプルで一般的ですが、他の機能でバックエンドが必要になる可能性は十分にあります。
あ~なるほど、Honoの意義としてはCF Workersみたいなエッジ環境でSSRとして活用するのか
だからrenderer.tsxがあったりする訳か、そのjsxをssrできるみたいな話なのだろう
あと共通ロジックをapiエンドポイントとしてまとめておいた方が楽とかいろいろある
へぇ~

よし、ブラウザのlocalStorage使ってる処理は全部hono使わずにクライアントだけで完結するように修正しよう

というかsigninのfetchした後userId取得のfetchもしててめっちゃ時間かかってたけど、signinの返り値のdataからidも取れることに気付いた...!!
これこそがN + 1問題の解消みたいなやつなのかな?(知らん)

よしできた!ちゃんとlocalStorage保存されるようになったしuserIdもついでにちゃんと取得できてる
api側もフロント側もコード量かなり減らせたし、ログイン成功してpath遷移するまでの速度がめちゃくちゃ早くなって良き

ただsignInのレスポンスpromiseはキャッシュすべきなのか?
/authではbeforeLoad: checkSession
を設定して非認証ユーザの強制リダイレクト処理を書いているのでそもそも必要ないfetchはされない気もするが....
いやでもchrome devtoolsでlocalStorage消してもう一度signinページに来てsigninしようとするみたいな場合はキャッシュが生きるのか(まぁそんなこと普通のユーザはしないだろうけど)
localStorageには無いけどtanstack queryのキャッシュにはまだ残っているsigninの情報を使用して、すぐにsigninができるみたいな
じゃあちゃんと残しておくべきだな多分

参考

ただそもそも、なぜかsinginのキャッシュがずっとinactiveなんだよなぁ、なんでだ
queryClient.setQueryData(authKeys.signIn, userId)
が機能してないのか?
んん?mutationKeyというのを発見したぞ、これを使えないだろうか
確かにuseQueryの時は普通にqueryKeyというオプションが用意されてたのにuseMutationの時だけそういうオプションが無いのってなんでだろうとか思ってたけど、普通にあったってことか?
使ってみたけどキャッシュに登録されないなぁ。じゃあmutationKeyというのは何なんだろう
うーん、キャッシュするものではなさそう。だめか。
gemini
はい、TanStack Query v5 (旧 React Query v5) の useMutation
フックに導入された mutationKey
オプションについて説明します。
参照された GitHub ディスカッションにもあるように、mutationKey
は v5 で新たに追加された オプショナルな オプションです。
mutationKey
とは何か?
mutationKey
は、特定の useMutation
インスタンスを一意に識別するためのキーです。useQuery
における queryKey
がクエリキャッシュを識別するのと概念的には似ていますが、役割は少し異なります。
主な目的・メリット
-
DevTools での識別性向上:
- React Query DevTools を使用している場合、
mutationKey
を設定しておくと、どのミューテーションが実行されているか、その状態(pending, success, errorなど)を特定しやすくなります。キーがない場合、ミューテーションは DevTools 上で区別しにくくなります。
- React Query DevTools を使用している場合、
-
プログラムからのミューテーションへのアクセス:
-
queryClient
を通じて、特定のミューテーションの状態やインスタンスにアクセスする必要がある場合に役立ちます。例えば、queryClient.getMutationCache().find({ mutationKey: [...] })
のようにして、特定のキーを持つミューテーションの状態を取得したり、操作したりすることが可能になります。(これは高度なユースケースです)
-
-
コードの明確性と整理:
- アプリケーション内に多数のミューテーションが存在する場合、それぞれに
mutationKey
を設定することで、どのミューテーションが何を担当しているのかがコード上でより明確になる可能性があります。
- アプリケーション内に多数のミューテーションが存在する場合、それぞれに
queryKey
との主な違い
-
キャッシュ:
queryKey
はクエリの結果をキャッシュするために不可欠です。一方、mutationKey
はミューテーションの「結果」自体をキャッシュするためのものではありません。ミューテーションは通常、サーバーの状態を変更するアクションであり、その結果をキャッシュすることは少ないです。 -
必須性:
queryKey
はuseQuery
に必須ですが、mutationKey
はuseMutation
には オプショナル(任意) です。設定しなくてもミューテーションは正常に機能します。 -
主な役割:
queryKey
の主な役割はデータキャッシュと再取得の管理です。mutationKey
の主な役割は、ミューテーションインスタンス自体の識別です。
どういう時に使うか?
- React Query DevTools でデバッグを容易にしたい場合。
-
queryClient
を介して特定のミューテーションの状態を外部から参照・操作したい高度なケース。 - コードベースが大きく、多数のミューテーションがあり、それらを明確に区別したい場合。
基本的な使い方では必須ではない
onSuccess
や onError
、onSettled
コールバック内で queryClient.invalidateQueries()
を使って関連するクエリを無効化する、といった基本的な使い方においては、mutationKey
は必須ではありません。クエリの無効化は、従来通り、無効化したいクエリの queryKey
を指定して行います。
コード例
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { addTodo } from './api'; // 仮のAPI関数
function AddTodoForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
// ここで mutationKey を設定
mutationKey: ['addTodo'],
mutationFn: addTodo, // APIを呼び出す関数
onSuccess: () => {
// 成功時に 'todos' クエリを無効化
// ここで mutationKey は直接使わないことが多い
queryClient.invalidateQueries({ queryKey: ['todos'] });
console.log('Todo added!');
},
onError: (error) => {
console.error('Error adding todo:', error);
}
});
const handleSubmit = (event) => {
event.preventDefault();
const title = event.target.elements.title.value;
if (title) {
mutation.mutate({ title }); // mutationを実行
event.target.reset();
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="New Todo" disabled={mutation.isPending} />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
{mutation.isError && (
<div style={{ color: 'red' }}>Error: {mutation.error.message}</div>
)}
</form>
);
}
まとめ
-
mutationKey
は TanStack Query v5 のuseMutation
に追加されたオプショナルなキー。 - 主な目的は、DevToolsでの識別や、
queryClient
を使ったプログラムからのアクセス。 -
queryKey
のようにミューテーション結果のキャッシュに使われるわけではない。 - 基本的なミューテーションの利用(実行とクエリ無効化)には必須ではない。
デバッグや高度な制御が必要な場合に設定すると便利な機能です。

はーなるほど、useQueryでデータ取得しようとしないとinactiveになるんだ、知らなかった
claude
この状況はおかしくありません。signinのキャッシュがInactiveになっている理由はTanStack Queryの設計によるものです。
キャッシュがInactiveになっている理由は以下の通りです:
-
直接設定されたデータの扱い:
queryClient.setQueryData
で直接設定されたデータは、アクティブなサブスクリプション(useQueryフック)がない場合、Inactiveとマークされます。 -
キャッシュステータスの意味:
- Inactive:データはキャッシュに存在するが、現在アクティブに使用されていない
- Active:useQueryフックを通じて現在サブスクライブされている
-
想定される動作:
あなたのケースでは、signinデータをsetQueryDataで設定した後、それを使用するuseQueryがまだアクティブでない(あるいは別ページにある)ため、Inactiveとして表示されています。
スクリーンショットからは、["todos"]がActiveで["signIn"]がInactiveになっていることが確認できます。これは["todos"]データがどこかでuseQueryによって使用されていて、["signIn"]データはsetQueryDataで設定されたものの、現在のページでuseQueryによって読み取られていないことを示しています。
この状態は問題ありません。データは依然としてキャッシュに保存されており、必要なときにuseQueryでアクセスできます。ユーザーIDがInactiveでも、その値は保持されており、useQueryでアクセスすると再びActiveになります。
試してみるか
todo一覧ページ内でボタンを押したらsigninプロミスをuseQueryでfetchする感じでやってみるか
いや普通にレンダー時にfetch発火させちゃって良いか、他のやつと同じように

うーん、これ何に対してfetchすればいいんだ?
基になってるやつはsupabaseのsigninメソッド実行してるだけで自分が立てたhonoのapiのurlとか取得できないしなぁ
あー普通にまったく同じsignIn.tsをこっちでも実行すれば良いのかも

できた!!ちゃんとどっちもStaleになった
へぇー、useQuery(useSuspenseQuery)で同じkeyに対するリクエストが送られて初めてそのキャッシュされたプロミスは反応してinactiveからstaleになってくれるんだなぁ、勉強になった
疑問
- signInリクエストは1度やったらもう普通やらないと思うのでキャッシュする必要性ほぼ無い気もするが、キャッシュしないで良いか?どうすべきだ?
- todos一覧のキャッシュに関しても、追加/削除/編集したらtodo一覧のデータはすでに変わってしまうので、同じキャッシュを使用することはできないので、結局キャッシュって意味なくないか...??
- ブラウザを再読み込みする訳ではない、かつデータ内容がまったく変わっていない可能性が普通にあるもの、に対して、何度もリクエスト送りたいなんてケースあるか...??
- キャッシュの必要性について、やはりまだよくわかっていない
- あーでも5分経過してstaleがinactiveになった時に再fetchしてくれるのが助かるのかなぁ
- 現状だとuseEffectではなくレンダー時に直接コンポーネント直下でfetchに繋がるuse〇〇を実行しているが、そうではなくuseEffect内でsetStateとか使っちゃうと無限レンダーになってしまうってこと?
- いやそんなことはない、当然そんなやばすぎることはuseEffect使ったからと言って起こらない
- あー多数のユーザが同時編集している可能性があり、その場合はtanstack queryみたいなキャッシュ&自動fetchという機能が役立つということ?
- いやでもそれは自動fetchが役立つのであってキャッシュは関係ない
- いや関係あるか。自動fetchの際に該当プロミスのkeyを使用した更新処理が行われていない場合、中身同じであると判定できて、その場合はそのままキャッシュを返すことができる
- だがそうなるとそれは多数ユーザ同時編集という概念とは関係ない話で、自分のみを想定した場合になるけれども
- ん、?だがそもそもuseMutationで更新する際に再レンダーしてUI変更もちゃんとやるので、その時点で状態は最新であることが保証できている。だから自動fetchの必要が無いよな..?
- ん、そこで多数ユーザ同時編集という概念が登場するのか。他の人は更新してるかもしれない、ていうことを考えねばいけないので定期的な自動fetchはやはり必須だよね、みたいな
- あとuseEffectでデータ更新すると、レンダー後にvalue更新してもう1回レンダーすることになって、画面のちらつきにもなるし効率性も悪いし、という問題(="ウォーターフォール問題")があるのに対して、tanstack query使うと内部で良きにやってくれるからその心配はなく、かつサーバへの不要なリクエストはしないで済む、そしてそれを活かすためにはuseEffectではなくtanstack queryのようなキャッシュ&自動fetch機能が必要である。そしてそれを機能させるにはReactの書き方の制約とかも相まってコンポーネント直下に書く必要がある。だから「tanstack queryを使う かつ それはコンポーネント直下でないといけない」という今の状況が生まれている、と言える...!?
- つまり意義(積極的理由)と制約(消去法的理由)の2つがあるのかなぁという感じ
- ブラウザを再読み込みする訳ではない、かつデータ内容がまったく変わっていない可能性が普通にあるもの、に対して、何度もリクエスト送りたいなんてケースあるか...??

claudeに聞いたら、かなりいろいろ意義がありそうだなぁとわかった
- バックグラウンドでfetchすることで、ローカルという枠組みを超えたキャッシュが実現できてるとも言えるよね
- 多数ユーザ同時編集についての話が多分これ
- たしかに
- ローカルだけの観点で言っても、複数コンポーネントから同じデータ取得しようとしたらキャッシュが便利に決まってるやん
- たしかに
- あとページネーション(ただのページ遷移)でも、mountだとfetch反応してしまうがそうではなくキャッシュ使うだけでええやんみたいなこともできそう
- optimistic updatesとかライブラリ側がやってくれるから助かるやん
- たしかに。つまり何かを避けるためではなく何か凄い積極的な理由を実現するためという側面もあるんだなぁ、たしかに、となった
- 単純に、宣言的記述だから再利用性上がって最高やん
- (まぁそれはtsquery使わずとも関数自分で作れば同じな気もするけど)
- useSuspenseQueryとか使っちゃえばisLoadingとかの変数不要になってめっちゃ便利やん
- たしかに

というまとめをclaudeに精査してもらった
claude 3.7
あなたのまとめは非常に的確です。TanStack Queryの価値を様々な角度から捉えられています。少し補足と精査をしてみます:
まとめの精査
✅ バックグラウンドfetchによるグローバル視点のキャッシュ
- 正確な理解です。ローカルキャッシュに最新のサーバーデータを定期的に同期することで、「ローカル」の枠を超えた価値を提供しています。
- 多数ユーザー環境では特に重要な機能です。
✅ 複数コンポーネントでの共有キャッシュ
- その通りです。これはアプリケーション内でのデータ整合性を保ちつつ、パフォーマンスも向上させる重要な利点です。
✅ ページネーション/ページ遷移での恩恵
- 正確です。特にSPAでページ間を行き来する場合、前のページに戻ったときに即座にデータを表示できる点は大きな利点です。
- また、無限スクロールやページネーションの実装も大幅に簡略化されます。
✅ Optimistic Updates
- おっしゃる通り、これは「何かを避ける」というネガティブな理由ではなく、「より良いUXを実現する」という積極的な理由です。
- ユーザーアクションに対する即時フィードバックは現代UIの重要な要素です。
✅ 宣言的APIと再利用性
- 確かに独自関数でも実現可能ですが、TanStack Queryの宣言的APIは:
- チーム内での一貫性を保ちやすい
- エッジケース(キャンセル、競合状態など)も含めて適切に処理
- フレームワークと深く統合されている
- メンテナンスの負担が減る
✅ Suspenseとの統合
- おっしゃる通りです。特にReact 18以降、SuspenseとTanStack Queryの組み合わせはコードをさらにシンプルにします。
- ローディング状態の宣言的な処理が可能になり、コンポーネントのロジックに集中できます。
付け加えるとしたら
- 開発者体験 (DX): 単なる機能性だけでなく、開発プロセス全体が改善されることも大きな価値です。
- 型安全性: TypeScriptとの相性が良く、データ取得から表示までの型の一貫性を保てます。
- デバッグのしやすさ: DevToolsが提供され、キャッシュ状態やクエリの挙動を可視化できます。
全体として、あなたのまとめは本質を捉えています。TanStack Queryは単なる「キャッシュライブラリ」ではなく、モダンReactアプリケーションのデータフローを根本的に改善するツールと言えるでしょう。
このあたりもあるのか↓
- 独自関数でも再利用性上げることできるとはいっても、書き方が完全に自由になるのでチーム内でばらつきとか出る可能性が高く、良くない
- tsqueryだと書き方の制約があるからこそ統一性が保てて良い
- 修正もテストも楽になる
- tsとの相性が良く型安全性が高まる
- デバッグツールが用意されてて便利

よし、ということでキャッシュやtsqueryの意義については腑に落ちたので開発を進める
とりあえず非同期処理は全部キャッシュするので良いかなーていう感じでゆる~くやっていく
(signInのやつは1回しか普通実行しないからキャッシュの必要性無くない?みたいな応用はまた次の機会で。)

次はsignUpもキャッシュ追加する
ほぼsignInのコピペでいけた
signUp.tsでuserId返すように修正したり、signInとsupabaseのレスポンスの型が少し違うらしく!data.userまで確認しないといけなかったり、ていうぐらいかな

次これやる。signup時にユーザをテーブルに登録
これはブラウザ(, web storage api)関係無いのでhonoのapi作ってそこでクエリ関数実行する感じにしよう

多分バックエンドはこれで良いはず
あとはフロントエンドでpostリクエスト送る

できた!ユーザをテーブルにINSERT
- onSuccessをasyncに変えて、中でhono apiへリクエスト送る感じにした
- それ以外の方法/より良い方法 があるのかはすごく気になる
- リクエスト2回送ってるので、やはり遅い感じが否めない
- onSuccessを2つ作って分離して見易くするとかできるのかなぁ
- mutationFnのsignUp関数でdata.user.idを返してたところをdata.userにすることで、User.idとUser.emailとしてどちらも使えるように抽象化した

claudeに聞いてみた、なるほど~~ただ数行分を関数に分ければ良いのか
claude
このコードを改善する方法をいくつか提案します。確かに、onSuccess
を分割することで、コードをより整理できるでしょう。
// useSignUp.ts
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router"
import { authKeys } from "../-key/key"
import { AuthProps } from "../-types"
import { signUp } from "./signUp"
import { fetchRegisterUser } from "./fetchRegisterUser"
export const useSignUp = () => {
const queryClient = useQueryClient()
const navigate = useNavigate()
const { mutate } = useMutation({
mutationFn: (credentials: AuthProps) => signUp(credentials),
onSuccess: async (user) => {
if (!isValidUser(user)) return
const registrationSuccess = await handleUserRegistration(user.email)
if (!registrationSuccess) return
handleSuccessfulSignUp(user)
}
})
// ユーザーの有効性を確認する関数
const isValidUser = (user: any): boolean => {
if (!user || !user.email) {
alert('サインアップの結果、userもしくはuserのemailが取得できませんでした')
console.error("user: ", user)
console.error("user.email: ", user?.email)
return false
}
return true
}
// ユーザー登録を処理する関数
const handleUserRegistration = async (email: string): Promise<boolean> => {
const success = await fetchRegisterUser(email)
if (!success) {
alert('user registerのfetchの結果がfalsyでした')
return false
}
return true
}
// サインアップ成功時の処理を行う関数
const handleSuccessfulSignUp = (user: { id: string, email: string }) => {
queryClient.setQueryData(authKeys.signUp, user.id)
alert('サインアップ成功')
navigate({ to: `/todos/${user.id}` })
}
return mutate as (credentials: AuthProps) => void
}
主な改善点:
-
関数の分割:
onSuccess
内のロジックを3つの小さな関数に分割しました:-
isValidUser
: ユーザーオブジェクトのバリデーション -
handleUserRegistration
: ユーザー登録プロセス -
handleSuccessfulSignUp
: 登録成功後の処理
-
-
型の明示化:
user
パラメータに対して明示的な型を追加しました。 -
早期リターン: 各段階でエラーがあった場合に早期リターンするパターンを使用し、コードを読みやすくしました。
-
命名の改善:
success
をregistrationSuccess
に変更し、変数の目的をより明確にしました。
この改善により、各関数は一つの責任だけを持つようになり、コードの可読性と保守性が高まります。また、将来的にこれらの関数を他の場所で再利用することも容易になります。
関数を別ファイルに分離したら、ちゃんとuserがnullでないことを保証してるのに、userがnullの可能性があります、と赤線で型エラーが出る
へぇ、tsはその関数の内部まで見てくれる訳ではないのか、知見だ
なので関数はuserだけ確認するように変えてそれを実行しつつuser.emailがあるかどうかは別で直接書くことで解決した

signInの方も、同じように軽くリファクタ(と言ってもisValidUserIdを作っただけ)

todo新規作成(insert)を書いたがなぜかDBへのクエリエラー。ログ見てもほぼ情報が無い
別問題だと思うが、AuthのuserIdとusersテーブルのidが違ってしまっているのに気づいたので修正
ん!!それで再度試すと普通にinsertができるようになった。なぜだ...?
あーわかった
todosテーブルのuser_idはnullを許容しない設定でsupabase上でtable設計したのに、contentだけ送ってuser_idは送らずにリクエスト送ってしまってたからか
つまりテーブルの値が入るべきフィールドに入っていないリクエストがきたからクエリエラーになった
こういうケースだとあんなにそっけないエラーログが出るのか...
というかスタックトレースしかでないのか...覚えておこう。
supabaseのテーブル設計に準じていないリクエスト、みたいなやつだとsupabaseからの細かいエラーメッセージみたいなのは提供されない、ということなんだろうなぁ
んでAuthのuseridとusers.idが違かったのは今回の問題に関係しておらず、単に自分のミス
(まぁ統一性が無いのは依然問題なので、気付けて良かった&直せて良かった)
これ、さっき気付いて直したんだけど、過去の自分の不注意によりinsertなのにupdateという名前にしたり処理がごっちゃになっていたので、その影響で「渡すのはcontentだけで良いや、user_idは渡す必要ないし」とか思っていたのだろう

あとは削除/編集で終わりか
まず削除から

supabaseのdeleteメソッド
- バックエンド
普通にgetリクエストのAPI作ればいけるだろう
いや消すtodoのidを送らないといけないからpostリクエストか
- フロントエンド
削除してまたuseMutation内でinvalidateQuery走らせてtodo一覧のクエリを無効化することで更新すれば良さげかな

deleteできた!
最後はupdateだ

supabaseのupdateメソッド
普通にweb標準のwindow.prompt
関数で入力受け付けるか

ていうか、promptで受け付けちゃえばuseStateで状態持つ必要もないのか...良いなこれ

これはほんとに謎なのだが、なぜか<ul>要素にpadding-left:40px
が自動付与されてる(???)ので応急処置として以下を書いておいた
<ul style={{ paddingLeft: '0px' }}>
スタイルは面倒だから今回は無し、最低限のやつのみstyle属性にベタ書きする

というかこのapi側の最後のレスポンス部分、関数として共通化できないかなぁと思ってしようとしたけどresultの型それぞれ違うし、c
をpropsで渡すと型指定面倒そうだし、messageもそれぞれ違うからきついか。諦めよう
if (result === null) {
return c.json({
status: 'success',
data: [],
message: 'まだTodoはありません'
})
} else {
return c.json({
status: 'success',
data: result,
message: 'Todo取得完了'
})
}

update実装完了
だが更新したtodoが最後尾にいってしまい順番が変わるのが嫌だな
どうすれば良いだろう

あ、キャッシュのkeyをtodoKeys.allしか持っていないので、各todoのkeyも作ってそれだけ更新するみたいなことできそうだな
んーでもそもそも各todoのキャッシュを設定していないんだよな
各todoの詳細ページ作るつもりなかったから
あーinsertの際に設定することはできそうか
- 最初に
["todos"]
キーで設定してtodo一覧取得 - その後insertする度に
["todos", "$todoId"]
で追加- そこで
["todos"]
キーをinvalidateもすることで更新情報も画面に反映
- そこで
- updateする際は
["todos", "$todoId"]
をinvalidateするだけで良い- それならupdate後も順序は変わらなくなるはず

と思ったんだけどclaudeに聞いたらめっちゃ良さげな案を教えてくれた
要約
queryClient.invalidateQueries
は使わないで良いよ。
queryClient.getQueryData
とqueryClient.setQueryData
使ってキャッシュだけ更新しちゃってそれを["todos"]
キャッシュに上書きしちゃいな。
なるほど...!!!
ローカルで全部済ましてしまうことができるのか...天才的だ
勉強になった部分をコメントに書いた
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { fetchUpdateTodo } from "./fetchUpdateTodo"
import { todoKeys } from "../-key/key"
import { UpdateTodoProps } from "~/backend/supabase/types"
import { Todo } from "../-types"
export const useUpdateTodo = () => {
const queryClient = useQueryClient()
const { mutate } = useMutation({
mutationFn: (updateProps: UpdateTodoProps) => fetchUpdateTodo(updateProps),
onSuccess: (updatedTodo, variables) => {
console.log("react:updatedTodoは", updatedTodo)
const previousTodos = queryClient.getQueryData<Todo[]>(todoKeys.all)
if (!previousTodos) return
// 良い: updatedTodoが必要なくなるし引数で渡ってくるvariablesは同期的なものなので確実
const updatedTodos = previousTodos.map(todo =>
todo.id === variables.todoId ? { ...todo, content: variables.newContent } : todo
)
// 微妙: updatedTodo(非同期だからデータの完全性/確実性が担保されない)を貰う必要がある
const updatedTodos = previousTodos.map(todo =>
todo.id === updatedTodo.id ? { ...todo, content: updateTodo.content } : todo
)
queryClient.setQueryData(todoKeys.all, updatedTodos)
alert('更新成功')
}
})
return mutate
}
このvariablesという引数は何かという話に関して。
使う側でmutate({ ... })
として設定した引数がmutationFnの引数に渡ることは知ってたけど、onSuccessの第二引数(variables)にも渡るらしい

最初の自分の「各todoのキャッシュ(キー)も作ってそれぞれ更新する感じでいけるのでは」という案に対しても一応claudeに聞いてみたけどこれはまた今度検証すれば良いかな
claude
あなたの考えは非常に的確です!TanStack Queryのキー設計についての素晴らしい思考プロセスですね。
ポイント別分析
1. キー設計の階層化
-
todoKeys.all
(['todos']
) だけでなく、個別のtodoに対して['todos', todoId]
のようなキーを設けるアプローチは理想的です - これはReact Queryの「キー階層」の概念に完全に合致しています
2. インサートと更新の処理
- インサート時に両方のキャッシュを設定/更新する戦略は効率的です
- 個別todoの更新時にそのtodoのキーだけをinvalidateする発想は正確で、順序問題を解決します
3. 補足と推奨点
- キーの構造をよりシステマチックにするため、todoKeysオブジェクトを拡張するとさらに良いでしょう:
export const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
detail: (id: number | string) => [...todoKeys.all, id] as const,
} as const
こうすれば:
- 一覧の取得:
useQuery({ queryKey: todoKeys.all, ... })
- 特定のtodoの取得/更新:
useQuery({ queryKey: todoKeys.detail(123), ... })
このようなキー設計は、アプリケーションが成長してtodoの詳細ページを追加する場合にもシームレスに対応できます。
あなたの思考プロセスは、React Queryの設計思想にぴったり合っています。最初はシンプルなキー設計からスタートし、必要に応じて階層化していくアプローチは理にかなっていますね。
多分todo詳細ページとか作りたいならこっちの方が拡張性高かったりとかで良さげなんだろうな
同じようにこれもinvalidateQueriesではなく(get/set)QueryDataでいけるはずだし
ただ今回は別に詳細ページ不要なのでいいや
(useUpdateTodo.tsを編集するだけで良いので楽だし)

ということで更新後順序が変わってしまう問題も解決
だがキャッシュを更新してるだけであってそれを画面に反映してるだけなので、データベースでは更新すると順序が変わっている。なので、ブラウザリロードすると順序は変わってしまう...
これはいつかやろう

「更新後すぐ順序が変わってしまう」を、「更新後リロードしたら順序が変わってしまう」に変えただけで根本解決になっていないことに気付いたが、まぁ改善ではあるのでとりあえずok

暫定的にalertにしている部分をreact-hot-toastに変えよう

んー、、、コンポーネントではなくuse〇〇.tsとかcheck〇〇.tsとかでは<Toaster />を配置できないのでtoastが機能しないのか。。
しゃあない、そこはalertでいいや(これ良い対処法あるのか気になる)
よし、とりあえずtoastできるとこは全部toastにした

完成