tanstack-query
TanStack Query v5 patterns for React. Reference for queries, mutations, infinite scroll, and cache management.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
TanStack Query v5 patterns for React. Reference for queries, mutations, infinite scroll, and cache management.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Sync documentation with code changes. Audits git diff and updates ARCHITECTURE.md, DECISIONS.md, TASKS.md, CLAUDE.md, and README.md to reflect current implementation.
Efficiently develop Hono applications using Hono CLI. Supports documentation search, API reference lookup, request testing, and bundle optimization.
Mastra v1 API patterns and examples. Reference material for agents, tools, workflows, and streaming. Based on latest @mastra/core docs.
| name | tanstack-query |
| description | TanStack Query v5 patterns for React. Reference for queries, mutations, infinite scroll, and cache management. |
| context | fork |
| user_invocable | true |
Quick reference for @tanstack/react-query v5 patterns used in OpenNews.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
// Use factory pattern for type-safe keys
export const queryKeys = {
feed: {
all: ['feed'] as const,
list: (filters: { tag?: string }) => ['feed', 'list', filters] as const,
},
article: {
all: ['article'] as const,
detail: (topicId: string) => ['article', topicId] as const,
},
settings: ['settings'] as const,
sources: ['sources'] as const,
tags: ['tags'] as const,
} as const;
import { useQuery } from '@tanstack/react-query';
function useSettings() {
return useQuery({
queryKey: queryKeys.settings,
queryFn: () => api.get<Settings>('/api/v1/settings'),
});
}
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useUpdateSettings() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Partial<Settings>) => api.put('/api/v1/settings', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.settings });
},
});
}
This is the primary pattern for the daily feed with cursor-based pagination.
import { useInfiniteQuery } from '@tanstack/react-query';
interface FeedResponse {
days: DayWithTopics[];
nextCursor?: string;
}
function useFeed(tag?: string) {
return useInfiniteQuery({
queryKey: queryKeys.feed.list({ tag }),
queryFn: async ({ pageParam }) => {
const params = new URLSearchParams();
if (pageParam) params.set('cursor', pageParam);
if (tag) params.set('tag', tag);
params.set('limit', '3');
return api.get<FeedResponse>(`/api/v1/feed?${params}`);
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
// In component:
function Feed() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useFeed(selectedTag);
const allDays = data?.pages.flatMap((page) => page.days) ?? [];
// Intersection observer for infinite scroll
const observerRef = useRef<IntersectionObserver>();
const loadMoreRef = useCallback((node: HTMLDivElement | null) => {
if (isFetchingNextPage) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
});
if (node) observerRef.current.observe(node);
}, [isFetchingNextPage, hasNextPage, fetchNextPage]);
return (
<>
{allDays.map((day) => <DaySection key={day.date} day={day} />)}
<div ref={loadMoreRef} />
</>
);
}
function useDeleteSource() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => api.delete(`/api/v1/sources/${id}`),
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: queryKeys.sources });
const previous = queryClient.getQueryData<Source[]>(queryKeys.sources);
queryClient.setQueryData<Source[]>(queryKeys.sources, (old) =>
old?.filter((s) => s.id !== id),
);
return { previous };
},
onError: (_err, _id, context) => {
queryClient.setQueryData(queryKeys.sources, context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.sources });
},
});
}
function useArticle(topicId: string) {
return useQuery({
queryKey: queryKeys.article.detail(topicId),
queryFn: () => api.get<{ cached: boolean; content?: string }>(`/api/v1/article/${topicId}`),
enabled: !!topicId,
});
}
// Prefetch on hover for perceived speed
function TopicCard({ topic }: { topic: Topic }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.article.detail(String(topic.id)),
queryFn: () => api.get(`/api/v1/article/${topic.id}`),
staleTime: 60 * 1000,
});
};
return <Link to={`/article/${topic.id}`} onMouseEnter={handleMouseEnter}>...</Link>;
}