集成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
相关产品推荐
相关产品推荐

