Next.js组件状态变更后未重新渲染问题求助
问题:话题筛选后QuestionFeed组件未重新发起查询
我实现了一个带无限滚动的QuestionFeed组件,Navbar里的NavTopicFilter组件(作为Context API提供者)提供话题下拉菜单。预期用户选择话题后,QuestionFeed会查询对应话题的问题。现在能在QuestionFeed里立即看到选中的话题,但组件不会重新渲染或发起查询,只有切换标签页再切回来才会更新。
QuestionFeed.tsx
"use client"; import { ExtendedQuestion } from "@/types/db"; import { useIntersection } from "@mantine/hooks"; import { useInfiniteQuery } from "@tanstack/react-query"; import axios from "axios"; import { Loader2 } from "lucide-react"; import { FC, useEffect, useRef } from "react"; import { useSession } from "next-auth/react"; import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config/config"; import QuestionCard from "./QuestionCard"; import { useFilter } from "../FilterContext"; interface QuestionFeedProps { initialQuestions: ExtendedQuestion[]; topicName?: string; } const QuestionFeed: FC<QuestionFeedProps> = ({ initialQuestions, topicName, }) => { const lastPostRef = useRef<HTMLElement>(null); const { ref, entry } = useIntersection({ root: lastPostRef.current, threshold: 1, }); const { data: session } = useSession(); const { selectedTopic } = useFilter(); console.log("From feed"); const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( ["infinite-query"], async ({ pageParam = 1 }) => { const query = `/api/q/?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}&topicId=${selectedTopic}`; const { data } = await axios.get(query); return data as ExtendedQuestion[]; }, { getNextPageParam: (_, pages) => { return pages.length + 1; }, initialData: { pages: [initialQuestions], pageParams: [1] }, } ); useEffect(() => { if (entry?.isIntersecting) { fetchNextPage(); // Load more posts when the last post comes into view } }, [entry, fetchNextPage]); useEffect(() => { fetchNextPage(); }, [selectedTopic]); const questions = data?.pages.flatMap((page) => page) ?? initialQuestions; return ( <ul className="flex flex-col col-span-2 space-y-2"> <p>Selected Filter: {selectedTopic}</p> {questions?.map((question, index) => { const votesAmt = question.votes.reduce((acc, vote) => { if (vote.type === "UP") return acc + 1; if (vote.type === "DOWN") return acc - 1; return acc; }, 0); const currentVote = question.votes.find( // @ts-ignore (vote) => vote.userId === session?.user?.id ); if (index === questions.length - 1) { // Add a ref to the last post in the list return ( <li key={question.id} ref={ref}> <QuestionCard content={question.content} answerAmt={question._count?.answers} topicName={question?.topic?.name} votesAmt={votesAmt} currentVote={currentVote} key={question?.id} questionId={question.id} authorId={question.authorId} authorName={question.author?.username ?? ""} authorImage={question.author.image ?? ""} createdAt={question.createdAt} /> </li> ); } else { return ( <QuestionCard content={question.content} answerAmt={question._count?.answers} topicName={question?.topic?.name} votesAmt={votesAmt} currentVote={currentVote} key={question?.id} questionId={question.id} authorImage={question.author.image ?? ""} authorId={question.authorId} authorName={question.author?.username ?? ""} createdAt={question.createdAt} /> ); } })} {isFetchingNextPage && ( <li className="flex justify-center"> <Loader2 className="w-6 h-6 text-zinc-500 animate-spin" /> </li> )} </ul> ); }; export default QuestionFeed;
FilterContext.tsx
// FilterContext.tsx "use client" import React, { createContext, useContext, useState, ReactNode } from 'react'; interface FilterContextProps { children: ReactNode; } interface FilterContextValue { selectedTopic: string; handleFilterChange: (filter: string) => void; } const FilterContext = createContext<FilterContextValue | undefined>(undefined); export const FilterProvider: React.FC<FilterContextProps> = ({ children }) => { const [selectedTopic, setSelectedTopic] = useState(''); const handleFilterChange = (filter: string) => { setSelectedTopic(filter); }; return ( <FilterContext.Provider value={{ selectedTopic, handleFilterChange }}> {children} </FilterContext.Provider> ); }; export const useFilter = (): FilterContextValue => { const context = useContext(FilterContext); if (!context) { throw new Error('useFilter must be used within a FilterProvider'); } return context; };
NavTopicFilter.tsx
"use client"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/DropdownMenu"; import axios from "axios"; import { toast } from "sonner"; import { useQuery } from "@tanstack/react-query"; import { Topic } from "@prisma/client"; import { Icons } from "./Icons"; import { useFilter } from "./FilterContext"; interface NavTopicFilterProps extends React.HTMLAttributes<HTMLDivElement> {} export function NavTopicFilter({}: NavTopicFilterProps) { const { selectedTopic, handleFilterChange } = useFilter(); async function fetchPosts() { try { const resp = await axios.get(`/api/topic`); return resp.data; } catch (err) { console.error("Error fetching posts:", err); // Log the error for debugging toast.error("Something went wrong"); throw err; // Rethrow the error to propagate it further if needed } } const query = useQuery(["topics"], fetchPosts); /* const [selectedOption, setSelectedOption] = useState("አጠቃላይ"); // Default selected option */ if (query.isLoading) { // Handle loading state if needed return <div>Loading...</div>; } return ( <DropdownMenu> <DropdownMenuTrigger> <div className="flex flex-row items-center gap-1 hover:bg-zinc-100 font-bold"> <div className="pl-2 md:pl-3 text-white flex flex-row"> <span className="md:text-black sm:text-white">ርእስ</span> <Icons.filter className="h-6 w-6 md:text-black sm:text-white" /> </div> </div> </DropdownMenuTrigger> <DropdownMenuContent className="bg-white" align="end"> {query.data?.map((option: Topic) => ( <DropdownMenuItem asChild className="cursor-pointer" onSelect={(event) => { event.preventDefault(); handleFilterChange(option.id); }} key={option.id} > <option value={option.id}>{option.name}</option> </DropdownMenuItem> ))} </DropdownMenuContent> </DropdownMenu> ); }
问题原因及修复方案
核心问题
- React Query的
useInfiniteQuery查询键固定为["infinite-query"],当selectedTopic变化时,查询键未更新,React Query会认为是同一个查询,复用缓存数据,不会重新发起请求。 - 手动调用
fetchNextPage只会加载下一页数据,不会重置现有查询结果,导致新话题数据追加到旧数据后,而非替换。
修复步骤
- 将
selectedTopic加入查询键
修改useInfiniteQuery的查询键为["infinite-query", selectedTopic],这样话题变化时,React Query会识别为新查询,自动重新请求并重置数据。 - 移除依赖
selectedTopic的useEffect
查询键变化后React Query会自动触发新请求,无需手动调用fetchNextPage。 - 可选:调整初始数据逻辑
如果需要仅在首次加载时使用初始数据,可让initialData依赖selectedTopic,或者直接移除该配置,让React Query自行处理首次请求。
修改后的QuestionFeed关键代码片段
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( ["infinite-query", selectedTopic], // 加入selectedTopic作为查询键一部分 async ({ pageParam = 1 }) => { const query = `/api/q/?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}&topicId=${selectedTopic}`; const { data } = await axios.get(query); return data as ExtendedQuestion[]; }, { getNextPageParam: (_, pages) => { return pages.length + 1; }, // 可选:仅在未选话题时使用初始数据 initialData: selectedTopic ? undefined : { pages: [initialQuestions], pageParams: [1] }, } ); // 移除这个手动触发的useEffect // useEffect(() => { // fetchNextPage(); // }, [selectedTopic]);
额外优化
话题切换时可添加滚动到顶部的逻辑:
useEffect(() => { window.scrollTo({ top: 0, behavior: "smooth" }); }, [selectedTopic]);
内容的提问来源于stack exchange,提问作者Dolarious
相关产品推荐
相关产品推荐

