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

集成Tanstack Infinite Query到Tiptap Mention扩展遇阻:无法更新过滤器

问题:将Tanstack Infinite Query结果作为Tiptap Mention建议来源

我想把Tanstack Infinite Query返回的用户列表作为Tiptap Mention扩展的提及建议来源,但一直没成功。核心问题是:在Tiptap编辑器Mention扩展的suggestions配置的items函数中,无法更新Infinite Query的过滤器参数,导致查询结果始终为空或未定义。

附尝试代码:

"use client";

import tippy, { Instance, Props } from "tippy.js";
import { useEditor, EditorContent, ReactRenderer } from "@tiptap/react";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
import Mention from "@tiptap/extension-mention";
import { MentionList } from "./MentionList";
import { Editor } from "@tiptap/core"; // To type the `editor` instance
import { useInfiniteQuery } from "@tanstack/react-query";
import kyInstance from "@/lib/ky";
import { UserData, UsersPage } from "@/lib/type";
import { useCallback, useEffect, useState } from "react";

interface MentionListProps {
  items: UserData[];
  command: (item: { user: UserData }) => void;
}

export default function MyEditor() {
  const [mentionQuery, setMentionQuery] = useState("");

  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ["users", mentionQuery],
    queryFn: ({ pageParam }) => {
      const searchParams: Record<string, string> = {};
      if (pageParam) {
        searchParams.cursor = pageParam;
      }
      if (!!mentionQuery) {
        searchParams.filter = mentionQuery;
      }

      return kyInstance.get("/api/users", { searchParams }).json<UsersPage>();
    },
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  const Myitems = ({ query }: { query: string }) => {
    const {
      data,
      fetchNextPage,
      hasNextPage,
      isFetching,
      isFetchingNextPage,
      status,
    } = useInfiniteQuery({
      queryKey: ["users", query],
      queryFn: ({ pageParam }) => {
        const searchParams: Record<string, string> = {};
        if (pageParam) {
          searchParams.cursor = pageParam;
        }
        if (!!mentionQuery) {
          searchParams.filter = query;
        }

        return kyInstance.get("/api/users", { searchParams }).json<UsersPage>();
      },
      initialPageParam: null as string | null,
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    });

    const allUsers = data ? data.pages.flatMap((page) => page.users) : [];
    console.log("users", allUsers);
    return allUsers;
  };

  // const getMentionSuggestions = useCallback(async ({ query }: { query: string }) => {
  //   const allUsers = data ? data.pages.flatMap((page) => page.users) : [];
  //   setMentionQuery(query);
  //   // Trigger a refetch when the query changes
  //   await fetchNextPage();
  //   console.log("users", allUsers)
  //   return allUsers;
  // }, [data,fetchNextPage]);

  const editor: Editor | null = useEditor({
    extensions: [
      Document,
      Paragraph,
      Text,
      Mention.configure({
        HTMLAttributes: {
          class: "mention",
        },
        suggestion: {
          items: async ({ query }: { query: string }) => {
            setMentionQuery(query);
            const users = data?.pages.flatMap((page) => page.users);
            setTimeout(() => {
              const users = data?.pages.flatMap((page) => page.users);
              console.log("users", users);
              return users || [];
            }, 5000);
            return users || [];
          },
          render: () => {
            let reactRenderer: ReactRenderer<MentionListProps>;
            let popup: Instance<Props>;

            return {
              onStart: (props) => {
                reactRenderer = new ReactRenderer(MentionList, {
                  props,
                  editor: props.editor,
                });

                popup = tippy(document.body, {
                  getReferenceClientRect: () => {
                    if (props.clientRect) {
                      return props.clientRect() as DOMRect;
                    }
                    // Fallback to an empty DOMRect if `props.clientRect` is null
                    return new DOMRect();
                  },
                  appendTo: () => document.body,
                  content: reactRenderer.element,
                  showOnCreate: true,
                  interactive: true,
                  trigger: "manual",
                  placement: "bottom-start",
                });
              },
              onUpdate(props) {
                reactRenderer.updateProps(props);

                popup.setProps({
                  getReferenceClientRect: () => {
                    if (props.clientRect) {
                      return props.clientRect() as DOMRect;
                    }
                    // Return a default client rect or handle as needed
                    return new DOMRect(); // Provides a non-null fallback
                  },
                });
              },
              // onKeyDown(props) {
              //   return reactRenderer.ref?.onKeyDown(props);
              // },
              onExit() {
                popup.destroy();
                reactRenderer.destroy();
              },
            };
          },
        },
      }),
    ],
    content: `
      <p>
        What do you all think about the new <span data-mention="Winona Ryder"></span> movie?
      </p>
    `,
  });

  return (
    <div>
      <EditorContent editor={editor} />
    </div>
  );
}
可行方案思路

1. 分离查询与Mention触发逻辑

  • 不要在items函数内部调用setMentionQuery,状态更新会触发组件重渲染,但此时items的返回值已经确定,无法拿到最新查询数据。
  • 通过监听Tiptap的mention:activate和mention:exit事件,在组件层面控制查询的开启与关闭,避免无效请求。

2. 优化React Query配置

  • 统一使用外层的useInfiniteQuery,将mentionQuery作为查询键的一部分,确保关键词变化时自动触发新查询。
  • 开启enabled选项,只在用户激活提及功能时发起请求;关键词变化时调用refetch重置分页,获取新关键词的第一页数据。

3. 调整items函数职责

  • items函数仅返回当前已缓存的查询结果,查询触发逻辑交给组件状态监听处理,保证数据一致性。
  • 将无限加载逻辑(fetchNextPage)传递给Mention列表组件,在列表滚动到底部时触发加载更多。
修正后的核心代码示例
export default function MyEditor() {
  const [mentionQuery, setMentionQuery] = useState("");
  const [isMentionActive, setIsMentionActive] = useState(false);

  const {
    data,
    fetchNextPage,
    hasNextPage,
    refetch,
    status,
  } = useInfiniteQuery({
    queryKey: ["users", mentionQuery],
    queryFn: ({ pageParam }) => {
      const searchParams: Record<string, string> = {};
      if (pageParam) searchParams.cursor = pageParam;
      if (mentionQuery) searchParams.filter = mentionQuery;
      return kyInstance.get("/api/users", { searchParams }).json<UsersPage>();
    },
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    enabled: isMentionActive,
  });

  // 监听Mention激活/退出事件
  useEffect(() => {
    if (!editor) return;
    const handleActivate = () => setIsMentionActive(true);
    const handleExit = () => {
      setIsMentionActive(false);
      setMentionQuery("");
    };
    editor.on("mention:activate", handleActivate);
    editor.on("mention:exit", handleExit);
    return () => {
      editor.off("mention:activate", handleActivate);
      editor.off("mention:exit", handleExit);
    };
  }, [editor]);

  // 关键词变化时重置分页查询
  useEffect(() => {
    if (isMentionActive && mentionQuery) {
      refetch();
    }
  }, [mentionQuery, isMentionActive, refetch]);

  const editor = useEditor({
    extensions: [
      Document,
      Paragraph,
      Text,
      Mention.configure({
        HTMLAttributes: { class: "mention" },
        suggestion: {
          items: async ({ query }) => {
            setMentionQuery(query);
            return data?.pages.flatMap(page => page.users) || [];
          },
          render: () => {
            let reactRenderer: ReactRenderer<MentionListProps>;
            let popup: Instance<Props>;

            return {
              onStart: (props) => {
                reactRenderer = new ReactRenderer(MentionList, {
                  props: {
                    ...props,
                    fetchNextPage,
                    hasNextPage,
                    isFetching: status === "loading",
                  },
                  editor: props.editor,
                });

                popup = tippy(document.body, {
                  getReferenceClientRect: props.clientRect || (() => new DOMRect()),
                  appendTo: () => document.body,
                  content: reactRenderer.element,
                  showOnCreate: true,
                  interactive: true,
                  trigger: "manual",
                  placement: "bottom-start",
                });
              },
              onUpdate(props) {
                reactRenderer.updateProps({
                  ...props,
                  fetchNextPage,
                  hasNextPage,
                  isFetching: status === "loading",
                });
                popup.setProps({
                  getReferenceClientRect: props.clientRect || (() => new DOMRect()),
                });
              },
              onExit() {
                popup.destroy();
                reactRenderer.destroy();
              },
            };
          },
        },
      }),
    ],
    content: `<p>What do you all think about the new <span data-mention="Winona Ryder"></span> movie?</p>`,
  });

  return <EditorContent editor={editor} />;
}
额外注意事项
  • 在MentionList组件中添加滚动监听,触发fetchNextPage实现无限加载。
  • 利用React Query的缓存机制,避免重复请求相同关键词的数据。
  • 处理查询加载状态,在列表中展示加载提示,提升用户体验。

内容的提问来源于stack exchange,提问作者darel noutsa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 04:18:10