Переглянути джерело

[Auth] Recovery page: useQuery dedupes generate across StrictMode mount

Replace useMutation+useRef guard with useQuery keyed on
'recovery-code-generate' (staleTime/gcTime Infinity, refetchOnMount
false). React 19 StrictMode dev double-mounted the page and the second
instance's useMutation never observed the first instance's response,
leaving the UI stuck on "Generating...". useQuery dedupes by key across
mount/remount/StrictMode and persists the result in the cache. Server
overwrites users.recovery_code on each call, so a true page reload still
regenerates as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User 2 місяців тому
батько
коміт
f71cb76
1 змінених файлів з 22 додано та 28 видалено
  1. 22 28
      src/app/(auth)/recovery/page.tsx

+ 22 - 28
src/app/(auth)/recovery/page.tsx

@@ -1,17 +1,20 @@
 "use client";
 
-import { useEffect, useRef } from "react";
 import { useRouter } from "next/navigation";
-import { useMutation } from "@tanstack/react-query";
+import { useQuery } from "@tanstack/react-query";
 import { getSupabaseBrowserClient } from "@/lib/supabase/client";
 import { RecoveryCodeDisplay } from "@/components/auth/recovery-code-display";
 
+// useQuery (not useMutation) so TanStack Query dedupes across StrictMode
+// double-mount and post-signup navigation churn. The server overwrites
+// users.recovery_code on each call, so re-firing on a true page reload is
+// fine — but within one browser session we want exactly one fire.
 export default function RecoveryPage() {
   const router = useRouter();
-  const hasRequested = useRef(false);
 
-  const generateMutation = useMutation({
-    mutationFn: async () => {
+  const { data, isPending, error } = useQuery({
+    queryKey: ["recovery-code-generate"],
+    queryFn: async () => {
       const supabase = getSupabaseBrowserClient();
       const {
         data: { session },
@@ -25,25 +28,20 @@ export default function RecoveryPage() {
           Authorization: `Bearer ${session.access_token}`,
         },
       });
-
-      const data = (await res.json()) as { code?: string; error?: string };
-
-      if (!res.ok) {
-        throw new Error(data.error || "Failed to generate recovery code");
+      const body = (await res.json()) as { code?: string; error?: string };
+      if (!res.ok || !body.code) {
+        throw new Error(body.error || "Failed to generate recovery code");
       }
-
-      return data.code!;
+      return body.code;
     },
+    staleTime: Infinity,
+    gcTime: Infinity,
+    retry: 1,
+    refetchOnWindowFocus: false,
+    refetchOnMount: false,
+    refetchOnReconnect: false,
   });
 
-  useEffect(() => {
-    if (!hasRequested.current) {
-      hasRequested.current = true;
-      generateMutation.mutate();
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);
-
   function handleConfirm() {
     router.push("/");
   }
@@ -52,21 +50,17 @@ export default function RecoveryPage() {
     <>
       <h1 className="mb-6 text-2xl font-bold text-center">Recovery Code</h1>
 
-      {generateMutation.isPending && (
+      {isPending && (
         <p className="text-center text-foreground/60">Generating your recovery code...</p>
       )}
 
-      {generateMutation.error && (
+      {error && (
         <p className="text-center text-red-500" role="alert">
-          {generateMutation.error instanceof Error
-            ? generateMutation.error.message
-            : "Something went wrong."}
+          {error instanceof Error ? error.message : "Something went wrong."}
         </p>
       )}
 
-      {generateMutation.data && (
-        <RecoveryCodeDisplay code={generateMutation.data} onConfirm={handleConfirm} />
-      )}
+      {data && <RecoveryCodeDisplay code={data} onConfirm={handleConfirm} />}
     </>
   );
 }