You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
};
"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>
  );
}

问题原因及修复方案

核心问题

  1. React Query的useInfiniteQuery查询键固定为["infinite-query"],当selectedTopic变化时,查询键未更新,React Query会认为是同一个查询,复用缓存数据,不会重新发起请求。
  2. 手动调用fetchNextPage只会加载下一页数据,不会重置现有查询结果,导致新话题数据追加到旧数据后,而非替换。

修复步骤

  1. 将selectedTopic加入查询键
    修改useInfiniteQuery的查询键为["infinite-query", selectedTopic],这样话题变化时,React Query会识别为新查询,自动重新请求并重置数据。
  2. 移除依赖selectedTopic的useEffect
    查询键变化后React Query会自动触发新请求,无需手动调用fetchNextPage。
  3. 可选:调整初始数据逻辑
    如果需要仅在首次加载时使用初始数据,可让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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 04:45:59