Skip to content

Conversation

@akashdas99
Copy link
Owner

@akashdas99 akashdas99 commented Jan 31, 2026

Summary by CodeRabbit

Release Notes

  • Refactor

    • Updated application architecture with improved data fetching and state management systems for better performance and responsiveness
    • Streamlined modal management and user state handling throughout the app
  • Chores

    • Updated core dependencies to support enhanced architecture

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Jan 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
undoubt-next Ignored Ignored Jan 31, 2026 0:39am

@coderabbitai
Copy link

coderabbitai bot commented Jan 31, 2026

📝 Walkthrough

Walkthrough

This pull request migrates the application's state management from Redux (RTK Query) to React Query and Zustand. Redux-based store setup, API slices, and dispatched actions are replaced with React Query hooks and a Zustand UI store. Components are updated to use new data-fetching and cache-invalidation patterns, and the root provider is switched to QueryProvider with client-side hydration support.

Changes

Cohort / File(s) Summary
Redux Store Removal
lib/store/store.ts, lib/store/hooks/index.ts, lib/store/user/user.ts, lib/store/questions/question.ts, lib/store/ui/deleteModalSlice.ts
Removed Redux store configuration, typed hooks wrappers, RTK Query API slices for user and questions, and Redux slice for delete modal state. All public store exports and dispatch-based APIs eliminated.
React Query Infrastructure
lib/api.ts, lib/getQueryClient.ts, lib/providers/QueryProvider.tsx, lib/queries/user.ts, lib/queries/questions.ts
Added lightweight HTTP client, QueryClient singleton configuration, QueryClientProvider wrapper, and new React Query hooks for user profile and questions data fetching with cache key management.
Zustand UI Store
store/useUIStore.ts, store/useMulti.ts
Introduced Zustand-based store for managing delete-modal UI state, with dedicated action creators and selector helper hook for optimized multi-property selection.
Root Provider & Layout
app/layout.tsx, app/StoreProvider.tsx
Replaced Redux StoreProvider with React Query QueryProvider; removed PrefetchUser component and related imports.
User Profile Hooks Migration
components/answer/addAnswer.tsx, components/answer/answerCard.tsx, components/common/profileDropdown.tsx, components/profile/imageUpload.tsx
Updated components to use new useProfile and useInvalidateProfile hooks from React Query instead of Redux-based useGetProfileQuery and dispatch-based invalidation.
Authentication Flow Updates
components/auth/loginForm.tsx, components/auth/registerForm.tsx
Replaced Redux dispatch-based profile tag invalidation with direct useInvalidateProfile() hook calls in login/register success handlers.
Logout & Menu Updates
components/common/menuItems.tsx, components/common/prefetchUser.tsx
Updated logout flow to use useInvalidateProfile() hook; deleted prefetch-on-mount component entirely.
Question List & Infinite Scroll
components/question/infiniteQuestionList.tsx, components/question/questionList.tsx, components/question/questionSearch.tsx
Refactored infinite question list to use useQuestionsInfinite and useUserVotes hooks; replaced RTK Query setup with React Query. Added server-side prefetch with HydrationBoundary in list wrapper; simplified search to use new hook-based approach.
Question Card & Voting
components/question/questionCard.tsx, components/question/questionVoteButton.tsx
Migrated question type imports from Redux store to React Query module; replaced Redux dispatch-based delete modal opening with Zustand selector; updated vote button to use React Query hooks and direct cache mutations via useQueryClient.
Delete Modal Modal Management
components/question/questionDeleteModal.tsx
Replaced Redux slice state/actions with Zustand UI store selectors; switched RTK Query cache updates to React Query's useQueryClient and setQueriesData for removing deleted questions.
Component API & Type Updates
components/ui/userImage.tsx
Adjusted UserImage prop type to explicitly allow null user prop alongside undefined.
Dependencies
package.json
Removed @reduxjs/toolkit and react-redux; added @tanstack/react-query, zustand, and @tanstack/eslint-plugin-query devDependency.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #56: Directly related—this PR removes PrefetchUser and usePrefetch exports that the referenced PR previously added, indicating a shift away from that prefetch pattern.
  • PR #61: Modifies the question voting and useUserVotes logic; shares Question types and vote-related hooks that overlap with this PR's refactor.
  • PR #65: Directly related—this PR replaces Redux deleteModalSlice with Zustand-based UI store and updates delete-modal components accordingly, reversing the Redux additions in the referenced PR.

Poem

🐰 Hops away from Redux chains,
React Query now sustains,
Zustand for modals, cache so clean,
Simplest state we've ever seen! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main change: replacing Redux Toolkit with React Query and Zustand for state management across the web application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/web/reactquery-zustand

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/components/common/profileDropdown.tsx (1)

1-1: ⚠️ Potential issue | 🔴 Critical

Missing "use client" directive.

This component uses React hooks (useRouter, useInvalidateProfile, useProfile) but lacks the "use client" directive at the top. Next.js will throw a runtime error when trying to use hooks in what it interprets as a Server Component.

🐛 Proposed fix
+"use client";
+
 import { logoutUser } from "@/actions/auth";
apps/web/components/question/questionVoteButton.tsx (1)

104-116: ⚠️ Potential issue | 🟠 Major

Missing rollback logic for failed optimistic updates.

If voteOnQuestionAction fails, the optimistic cache updates persist, leaving the UI in an inconsistent state with the server. Consider capturing the previous state and rolling back on error.

🛡️ Proposed fix with rollback
   const handleVote = async (voteType: VoteType) => {
     if (isEmpty(user)) return router.push("/login");
 
     const isRemoving = currentVote === voteType;
     const newVoteValue = isRemoving ? null : voteType === "like" ? 1 : -1;
 
+    // Capture previous state for rollback
+    const previousVote = cachedUserVote;
+    const previousQuestion = cachedQuestion;
+
     // Optimistically update both caches
     updateUserVotesCache(newVoteValue);
     updateQuestionsCache(voteType);
 
     // Perform server action
-    await voteOnQuestionAction(questionId, isRemoving ? "remove" : voteType);
+    const result = await voteOnQuestionAction(questionId, isRemoving ? "remove" : voteType);
+    
+    // Rollback on failure
+    if (!result?.success) {
+      updateUserVotesCache(previousVote);
+      // Restore previous question counts if needed
+      queryClient.setQueriesData<{
+        pages: PaginatedResponse[];
+        pageParams: number[];
+      }>({ queryKey: ["questions", "list"] }, (oldData) => {
+        if (!oldData || !previousQuestion) return oldData;
+        return {
+          ...oldData,
+          pages: oldData.pages.map((page) => ({
+            ...page,
+            data: page.data?.map((q) =>
+              q.id === questionId ? { ...q, likes: previousQuestion.likes, dislikes: previousQuestion.dislikes } : q
+            ),
+          })),
+        };
+      });
+    }
   };
apps/web/components/question/infiniteQuestionList.tsx (1)

45-47: ⚠️ Potential issue | 🟡 Minor

Avoid optional chaining for React keys.

Using question?.id as a key means if question is undefined, the key becomes undefined, which causes React warnings and breaks reconciliation. If page.data can contain undefined items, filter them out instead:

Proposed fix
-        {questions.map((question) => (
-          <QuestionCard key={question?.id} question={question} />
+        {questions.map((question) => (
+          <QuestionCard key={question.id} question={question} />
         ))}

Or if undefined items are possible in the response, filter them first on line 15:

-  const questions = data?.pages?.flatMap((page) => page.data) ?? [];
+  const questions = data?.pages?.flatMap((page) => page.data).filter(Boolean) ?? [];
🤖 Fix all issues with AI agents
In `@apps/web/lib/api.ts`:
- Around line 34-36: The current fetch handling returns null on non-OK responses
and blindly calls res.json(), which hides errors from React Query and crashes on
204/non‑JSON responses; update the logic in the API fetch code that awaits
fetch(url, fetchOptions) so that if !res.ok you throw an Error (include status
and statusText) to surface failures to React Query, and before calling
res.json() check for empty responses (e.g., res.status === 204) or verify
Content-Type includes application/json and return null or an appropriate empty
value when there is no JSON payload. Ensure you modify the branch around the res
variable returned by fetch(url, fetchOptions) rather than swallowing errors.
- Around line 1-20: The code uses baseUrl (const baseUrl =
process.env.NEXT_PUBLIC_BASEURL) without guarding for undefined which yields
URLs like "undefined/…"; update the initialization and/or the request function
(request) to default baseUrl to an empty string or validate it and treat
absolute endpoints (starting with "http") specially: ensure baseUrl is set to a
safe default (e.g., '') or throw a clear error, and when constructing url in
request use the safe base or return the endpoint unchanged if it is already an
absolute URL so requests never build "undefined/…".

In `@apps/web/lib/providers/QueryProvider.tsx`:
- Around line 7-12: Replace the useState-based QueryClient creation in
QueryProvider with a module-level getQueryClient() helper that returns a new
QueryClient when isServer is true and a singleton instance in the browser;
implement getQueryClient() to import isServer from `@tanstack/react-query` and to
use queryClientConfig when constructing the client, then call getQueryClient()
inside QueryProvider (instead of useState(() => new QueryClient(...))) so the
QueryClient survives App Router retries and preserves cache.

In `@apps/web/lib/queries/questions.ts`:
- Around line 100-120: useQuestionById currently reads the react-query cache
synchronously and doesn't subscribe to updates, so it won't reflect optimistic
changes; change it to a reactive implementation similar to useVoteByQuestionId
by using useSyncExternalStore (or subscribing to queryClient.getQueryCache()) to
subscribe to cache updates and return the current snapshot for queryKey
["questions","list"]; specifically, update the useQuestionById function to
register a subscribe callback (via useSyncExternalStore or queryCache.subscribe)
that triggers when the questions list changes and compute the current cached
question by iterating pages (same logic that searches page.data.find(q => q.id
=== questionId)), ensuring the hook returns updated likes/dislikes after
optimistic updates from questionVoteButton.tsx.
- Around line 81-98: useVoteByQuestionId currently reads the react-query cache
synchronously and doesn't subscribe to updates, causing stale UI after
optimistic updates; change it to a reactive read by using useQuery (queryKey:
["userVotes"]) with a select function that returns data?.[questionId] as
-1|1|null so the hook subscribes to the userVotes cache and re-renders when
updateUserVotesCache (used in questionVoteButton.tsx) mutates the cache;
alternatively implement a useSyncExternalStore subscriber to the
queryClient.getQueryCache() keyed to "userVotes" that returns the specific
question vote to ensure components update on cache changes.

In `@apps/web/package.json`:
- Line 58: The package.json lists the devDependency
"@tanstack/eslint-plugin-query" but apps/web/eslint.config.js doesn't reference
it; either remove the dependency from package.json or add the plugin to the
ESLint config: import/register the plugin in apps/web/eslint.config.js (e.g.,
include "@tanstack/eslint-plugin-query" in the plugins/overrides or add its
recommended config to extends) and enable any desired rules from that plugin so
the dependency is actually used; update package.json only if you choose to
remove the unused entry.
🧹 Nitpick comments (8)
apps/web/lib/getQueryClient.ts (1)

5-16: Cache()-based singleton is officially supported but optional; be aware of serialization overhead.

TanStack Query v5 docs explicitly document this pattern as optional for request-scoped QueryClient reuse. However, the primary recommendation is creating a fresh QueryClient per Server Component that needs prefetching. The cache() approach has a known tradeoff: each dehydrate(getQueryClient()) may serialize the entire client cache, adding serialization overhead. If minimizing payload is a concern, consider the per-component pattern instead.

apps/web/components/profile/imageUpload.tsx (1)

20-25: Consider adding error handling for failed uploads.

The upload() call can throw, which would leave the user without feedback. Consider wrapping in try/catch to show an error state.

💡 Suggested improvement
+  try {
     await upload("public/" + file.name, file, {
       access: "public",
       handleUploadUrl: "/api/user/image",
     });
     router.refresh();
     invalidateProfile();
+  } catch (error) {
+    console.error("Upload failed:", error);
+    // Consider adding user-facing error feedback
+  }
apps/web/store/useMulti.ts (1)

4-9: Consider adding a note about the minimum keys requirement.

The docstring is helpful, but the function accepts zero keys via the spread operator, which would return an empty object. This is likely fine but could be documented.

📝 Optional: Enhanced documentation
 /**
  * Select multiple properties directly from a store with shallow comparison
  *
  * `@example`
  * const { deleteModal } = useMulti(useUIStore, "deleteModal");
+ * const { a, b } = useMulti(useStore, "a", "b");
+ *
+ * `@param` store - A zustand store created with `create()`
+ * `@param` keys - One or more state property keys to select
  */
apps/web/lib/queries/questions.ts (1)

33-34: Consider exporting SearchResult and SearchResponse types.

These types are defined but not exported. If they're needed elsewhere (e.g., for testing or type reuse), consider exporting them.

♻️ Export types if needed elsewhere
-type SearchResult = { label: string; value: string };
-type SearchResponse = { data: { title: string; slug: string }[] };
+export type SearchResult = { label: string; value: string };
+export type SearchResponse = { data: { title: string; slug: string }[] };
apps/web/components/question/questionSearch.tsx (2)

15-17: Remove unnecessary async keyword.

The handleChange function no longer performs any asynchronous operations after the migration. The async keyword can be removed.

♻️ Remove async
-  const handleChange = async (search: string) => {
+  const handleChange = (search: string) => {
     setSearchValue(search);
   };

43-52: Use unique key instead of array index.

Using index as a key can cause issues with React's reconciliation when items are reordered or filtered. Use question.value (the slug) which is unique.

♻️ Use unique key
           data?.map((question, index) => (
             <Link
-              key={index}
+              key={question.value}
               className="p-3 "
               href={"/question/" + question?.value}
               onClick={onClick}
apps/web/components/question/questionVoteButton.tsx (1)

20-20: Duplicate VoteType definition.

VoteType is already defined in apps/web/data/questionVote.ts (including "remove"). Consider importing it to avoid duplication and ensure consistency.

♻️ Import existing type
+import type { VoteType } from "@/data/questionVote";
 // ...
-type VoteType = "like" | "dislike";

Note: The imported type includes "remove", which is used in handleVote when calling voteOnQuestionAction.

apps/web/components/question/questionCard.tsx (1)

16-16: Remove unused useUIStore import.

Only useUIStoreSelector is used in this component. The useUIStore import is unused.

♻️ Remove unused import
-import { useUIStore, useUIStoreSelector } from "@/store/useUIStore";
+import { useUIStoreSelector } from "@/store/useUIStore";

Comment on lines +1 to +20
const baseUrl = process.env.NEXT_PUBLIC_BASEURL;

type Params = Record<string, string | number | undefined>;

type FetchOptions = Omit<RequestInit, "body"> & {
params?: Params;
};

type MutationOptions = FetchOptions & {
body?: unknown;
};

async function request<T>(
endpoint: string,
options?: FetchOptions & { body?: BodyInit },
): Promise<T | null> {
const { params, ...fetchOptions } = options ?? {};

let url = `${baseUrl}${endpoint}`;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard missing NEXT_PUBLIC_BASEURL to avoid “undefined” URLs.
If the env var is unset, all requests will go to undefined/.... Default to a safe empty base or handle absolute endpoints.

🔧 Proposed fix
-const baseUrl = process.env.NEXT_PUBLIC_BASEURL;
+const baseUrl = process.env.NEXT_PUBLIC_BASEURL ?? "";
@@
-  let url = `${baseUrl}${endpoint}`;
+  let url = endpoint.startsWith("http")
+    ? endpoint
+    : `${baseUrl}${endpoint}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const baseUrl = process.env.NEXT_PUBLIC_BASEURL;
type Params = Record<string, string | number | undefined>;
type FetchOptions = Omit<RequestInit, "body"> & {
params?: Params;
};
type MutationOptions = FetchOptions & {
body?: unknown;
};
async function request<T>(
endpoint: string,
options?: FetchOptions & { body?: BodyInit },
): Promise<T | null> {
const { params, ...fetchOptions } = options ?? {};
let url = `${baseUrl}${endpoint}`;
const baseUrl = process.env.NEXT_PUBLIC_BASEURL ?? "";
type Params = Record<string, string | number | undefined>;
type FetchOptions = Omit<RequestInit, "body"> & {
params?: Params;
};
type MutationOptions = FetchOptions & {
body?: unknown;
};
async function request<T>(
endpoint: string,
options?: FetchOptions & { body?: BodyInit },
): Promise<T | null> {
const { params, ...fetchOptions } = options ?? {};
let url = endpoint.startsWith("http")
? endpoint
: `${baseUrl}${endpoint}`;
🤖 Prompt for AI Agents
In `@apps/web/lib/api.ts` around lines 1 - 20, The code uses baseUrl (const
baseUrl = process.env.NEXT_PUBLIC_BASEURL) without guarding for undefined which
yields URLs like "undefined/…"; update the initialization and/or the request
function (request) to default baseUrl to an empty string or validate it and
treat absolute endpoints (starting with "http") specially: ensure baseUrl is set
to a safe default (e.g., '') or throw a clear error, and when constructing url
in request use the safe base or return the endpoint unchanged if it is already
an absolute URL so requests never build "undefined/…".

Comment on lines +34 to +36
const res = await fetch(url, fetchOptions);
if (!res.ok) return null;
return await res.json();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t treat non‑OK responses as successful data.
Returning null on errors makes React Query treat failures as success and res.json() will throw on 204/non‑JSON responses.

🔧 Proposed fix
-  const res = await fetch(url, fetchOptions);
-  if (!res.ok) return null;
-  return await res.json();
+  const res = await fetch(url, fetchOptions);
+  if (!res.ok) {
+    const text = await res.text().catch(() => "");
+    throw new Error(`Request failed (${res.status}) ${text}`);
+  }
+  if (res.status === 204) return null;
+  const contentType = res.headers.get("content-type") ?? "";
+  if (!contentType.includes("application/json")) return null;
+  return (await res.json()) as T;
🤖 Prompt for AI Agents
In `@apps/web/lib/api.ts` around lines 34 - 36, The current fetch handling returns
null on non-OK responses and blindly calls res.json(), which hides errors from
React Query and crashes on 204/non‑JSON responses; update the logic in the API
fetch code that awaits fetch(url, fetchOptions) so that if !res.ok you throw an
Error (include status and statusText) to surface failures to React Query, and
before calling res.json() check for empty responses (e.g., res.status === 204)
or verify Content-Type includes application/json and return null or an
appropriate empty value when there is no JSON payload. Ensure you modify the
branch around the res variable returned by fetch(url, fetchOptions) rather than
swallowing errors.

Comment on lines +7 to +12
export default function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient(queryClientConfig));

return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

TanStack Query v5 QueryClient initialization pattern useState useMemo Next.js App Router best practice

💡 Result:

Best-practice (TanStack Query v5 + Next.js App Router)

Use a dedicated client Providers component and create the QueryClient via a getQueryClient() helper that:

  • creates a new client on the server (per request), and
  • reuses a module-level singleton in the browser (so it survives React retries/suspense during initial render). (tanstack.com)
// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider, isServer } from '@tanstack/react-query'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60_000, // common SSR/default choice to avoid immediate refetch
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined

function getQueryClient() {
  if (isServer) return makeQueryClient()
  if (!browserQueryClient) browserQueryClient = makeQueryClient()
  return browserQueryClient
}

export function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient()
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

How this compares to useState / useMemo

  • useState(() => new QueryClient()) is a common pattern in “pure client” setups (it’s even shown in TanStack’s SSR guide examples for non–App Router contexts). (tanstack.com)
  • For App Router + streaming/Suspense, TanStack’s Advanced Server Rendering guidance emphasizes ensuring the browser QueryClient is not recreated if React suspends/retries during the initial render, which is why the module-level singleton + getQueryClient() pattern is recommended there. (tanstack.com)

If you tell me whether you’re doing server prefetch + dehydrate/HydrationBoundary (or purely client fetching), I can tailor the exact file layout and hydration wiring.

Citations:


Update QueryClient initialization to follow TanStack Query v5 App Router best practice.
The current useState-based initialization should be replaced with a getQueryClient() helper using a module-level singleton pattern with isServer check. This prevents QueryClient recreation during React suspense/retry in App Router and is the recommended pattern for Next.js App Router per TanStack Query v5 official guidance.

Recommended implementation:

  • Create a getQueryClient() helper that returns a new client on server and a reused singleton in browser
  • Use isServer from @tanstack/react-query to distinguish environments
  • This ensures the client survives React retries during initial render and preserves cache state
🤖 Prompt for AI Agents
In `@apps/web/lib/providers/QueryProvider.tsx` around lines 7 - 12, Replace the
useState-based QueryClient creation in QueryProvider with a module-level
getQueryClient() helper that returns a new QueryClient when isServer is true and
a singleton instance in the browser; implement getQueryClient() to import
isServer from `@tanstack/react-query` and to use queryClientConfig when
constructing the client, then call getQueryClient() inside QueryProvider
(instead of useState(() => new QueryClient(...))) so the QueryClient survives
App Router retries and preserves cache.

Comment on lines +81 to +98
export function useVoteByQuestionId(questionId: string) {
const queryClient = useQueryClient();

// Look through all userVotes queries to find the vote for this question
const queriesData = queryClient.getQueriesData<Record<string, number | null>>(
{
queryKey: ["userVotes"],
},
);

for (const [, data] of queriesData) {
if (data?.[questionId] !== undefined) {
return data[questionId] as -1 | 1 | null;
}
}

return null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Non-reactive cache read will cause stale UI after optimistic updates.

useVoteByQuestionId reads from the query cache synchronously but doesn't subscribe to cache changes. When updateUserVotesCache in questionVoteButton.tsx updates the cache, components using this hook won't re-render because there's no subscription.

Consider using useQuery with select to create a reactive subscription, or use useSyncExternalStore for cache reads.

🐛 Proposed reactive implementation
 export function useVoteByQuestionId(questionId: string) {
   const queryClient = useQueryClient();
-
-  // Look through all userVotes queries to find the vote for this question
-  const queriesData = queryClient.getQueriesData<Record<string, number | null>>(
-    {
-      queryKey: ["userVotes"],
-    },
-  );
-
-  for (const [, data] of queriesData) {
-    if (data?.[questionId] !== undefined) {
-      return data[questionId] as -1 | 1 | null;
-    }
-  }
-
-  return null;
+  
+  // Subscribe to cache changes using useSyncExternalStore
+  const vote = React.useSyncExternalStore(
+    (onStoreChange) => {
+      return queryClient.getQueryCache().subscribe(onStoreChange);
+    },
+    () => {
+      const queriesData = queryClient.getQueriesData<Record<string, number | null>>({
+        queryKey: ["userVotes"],
+      });
+      for (const [, data] of queriesData) {
+        if (data?.[questionId] !== undefined) {
+          return data[questionId] as -1 | 1 | null;
+        }
+      }
+      return null;
+    },
+  );
+  
+  return vote;
 }
🤖 Prompt for AI Agents
In `@apps/web/lib/queries/questions.ts` around lines 81 - 98, useVoteByQuestionId
currently reads the react-query cache synchronously and doesn't subscribe to
updates, causing stale UI after optimistic updates; change it to a reactive read
by using useQuery (queryKey: ["userVotes"]) with a select function that returns
data?.[questionId] as -1|1|null so the hook subscribes to the userVotes cache
and re-renders when updateUserVotesCache (used in questionVoteButton.tsx)
mutates the cache; alternatively implement a useSyncExternalStore subscriber to
the queryClient.getQueryCache() keyed to "userVotes" that returns the specific
question vote to ensure components update on cache changes.

Comment on lines +100 to +120
// Helper hook to get a question from cache by ID
export function useQuestionById(questionId: string) {
const queryClient = useQueryClient();

const queriesData = queryClient.getQueriesData<{
pages: PaginatedResponse[];
}>({
queryKey: ["questions", "list"],
});

for (const [, data] of queriesData) {
if (data?.pages) {
for (const page of data.pages) {
const question = page.data?.find((q) => q.id === questionId);
if (question) return question;
}
}
}

return null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same non-reactive issue as useVoteByQuestionId.

useQuestionById also reads synchronously from the cache without subscribing to updates. After optimistic updates in questionVoteButton.tsx, the cachedQuestion won't reflect the new likes/dislikes counts until a re-render is triggered by something else.

Apply the same useSyncExternalStore pattern or refactor to use a reactive query approach.

🤖 Prompt for AI Agents
In `@apps/web/lib/queries/questions.ts` around lines 100 - 120, useQuestionById
currently reads the react-query cache synchronously and doesn't subscribe to
updates, so it won't reflect optimistic changes; change it to a reactive
implementation similar to useVoteByQuestionId by using useSyncExternalStore (or
subscribing to queryClient.getQueryCache()) to subscribe to cache updates and
return the current snapshot for queryKey ["questions","list"]; specifically,
update the useQuestionById function to register a subscribe callback (via
useSyncExternalStore or queryCache.subscribe) that triggers when the questions
list changes and compute the current cached question by iterating pages (same
logic that searches page.data.find(q => q.id === questionId)), ensuring the hook
returns updated likes/dislikes after optimistic updates from
questionVoteButton.tsx.

"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@tanstack/eslint-plugin-query": "^5.91.4",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find ESLint config files and verify `@tanstack/eslint-plugin-query` is registered
echo "=== Finding ESLint config files ==="
fd -H -a -t f 'eslint\.(config|rc|rc\.js|rc\.cjs|rc\.mjs|rc\.json|rc\.yaml|rc\.yml)$' . 2>/dev/null

echo ""
echo "=== Searching for tanstack/query or eslint-plugin-query references ==="
fd -H -a -t f 'eslint\.(config|rc|rc\.js|rc\.cjs|rc\.mjs|rc\.json|rc\.yaml|rc\.yml)$' . \
  -x rg -n 'tanstack/query|eslint-plugin-query' {} 2>/dev/null

echo ""
echo "=== Also checking for eslint.config.js or eslintrc patterns with broader search ==="
fd -H -a -t f '\.(eslintrc|eslint\.config)\.(js|cjs|mjs|json|yaml|yml)$' . 2>/dev/null

Repository: akashdas99/undoubt-next

Length of output: 259


🏁 Script executed:

#!/bin/bash
# Check if eslintConfig is in package.json
echo "=== Checking for eslintConfig in package.json ==="
rg -n 'eslintConfig|eslint-plugin' apps/web/package.json 2>/dev/null

echo ""
echo "=== Looking for any eslint references in config files ==="
rg -l 'eslint' . --type json --type js --type yaml 2>/dev/null | head -20

echo ""
echo "=== Checking for .eslintrc without extension ==="
fd -H -a '.eslintrc' . 2>/dev/null

echo ""
echo "=== Full search for eslint-plugin in all text files ==="
rg 'eslint-plugin-query|tanstack.*eslint' . --type-not node_modules 2>/dev/null

echo ""
echo "=== Check next.config.js if it exists and has eslint config ==="
if [ -f "apps/web/next.config.js" ]; then
  echo "Found next.config.js:"
  cat -n apps/web/next.config.js | head -50
fi

Repository: akashdas99/undoubt-next

Length of output: 1190


🏁 Script executed:

#!/bin/bash
# Read the eslint config file to see what plugins are configured
echo "=== Content of apps/web/eslint.config.js ==="
cat -n apps/web/eslint.config.js

echo ""
echo "=== Searching for 'plugins' in the eslint config ==="
rg -n 'plugins' apps/web/eslint.config.js -A 5 -B 2

Repository: akashdas99/undoubt-next

Length of output: 701


Add @tanstack/eslint-plugin-query to the ESLint config or remove it from dependencies.

The plugin is installed in package.json but not configured in apps/web/eslint.config.js (only eslint-plugin-drizzle is configured). Either wire it into the config and apply its rules, or remove the unused devDependency.

🤖 Prompt for AI Agents
In `@apps/web/package.json` at line 58, The package.json lists the devDependency
"@tanstack/eslint-plugin-query" but apps/web/eslint.config.js doesn't reference
it; either remove the dependency from package.json or add the plugin to the
ESLint config: import/register the plugin in apps/web/eslint.config.js (e.g.,
include "@tanstack/eslint-plugin-query" in the plugins/overrides or add its
recommended config to extends) and enable any desired rules from that plugin so
the dependency is actually used; update package.json only if you choose to
remove the unused entry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants