
frontend-development
by du2333
全栈个人博客,由Cloudflare全家桶驱动
SKILL.md
name: frontend-development description: Frontend development patterns for the Flare Stack Blog. Use when implementing data fetching with TanStack Query, creating route loaders, building infinite scroll, adding skeleton/pending states, or organizing UI components.
Frontend Development Patterns
This skill covers TanStack Query patterns, route loaders, component organization, and frontend best practices.
Data Fetching Architecture
The project follows TanStack Start / TanStack Query standard practices for seamless SSR and client-side caching.
1. Query Definition
Organize query definitions in features/<name>/queries/index.ts. Use a Query Key Factory pattern to centralize and type-safe your cache keys.
// features/posts/queries/index.ts
import { queryOptions, infiniteQueryOptions } from "@tanstack/react-query";
import { findPostBySlugFn, getPostsFn } from "../api/posts.api";
// 1. Define Query Key Factory (static arrays + functions)
export const POSTS_KEYS = {
all: ["posts"] as const,
// Parent keys (static arrays for prefix invalidation)
lists: ["posts", "list"] as const,
details: ["posts", "detail"] as const,
adminLists: ["posts", "admin-list"] as const,
// Child keys (functions for specific queries)
list: (filters?: { tagName?: string }) =>
["posts", "list", filters] as const,
detail: (idOrSlug: number | string) =>
["posts", "detail", idOrSlug] as const,
adminList: (params: GetPostsInput) =>
["posts", "admin-list", params] as const,
};
// 2. Define Query Options
export function postBySlugQuery(slug: string) {
return queryOptions({
queryKey: POSTS_KEYS.detail(slug),
queryFn: () => findPostBySlugFn({ data: { slug } }),
});
}
export function postsInfiniteQuery(tag?: string) {
return infiniteQueryOptions({
queryKey: POSTS_KEYS.list({ tagName: tag }),
queryFn: ({ pageParam }) =>
getPostsFn({ data: { cursor: pageParam, tag } }),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: undefined,
});
}
Key Pattern:
- Parent keys are static arrays (no
()) — used for prefix-based cache invalidation - Child keys are functions — used for specific queries with parameters
// Invalidation (no parentheses - static reference)
queryClient.invalidateQueries({ queryKey: POSTS_KEYS.adminLists });
// Query (with parentheses - function call)
useQuery({ queryKey: POSTS_KEYS.detail(postId) });
2. Route Loader (routes/<path>.tsx)
Use loader functions with ensureQueryData() or prefetchQuery() for SSR:
// $slug.tsx
export const Route = createFileRoute("/_public/post/$slug")({
loader: async ({ context, params }) => {
const post = await context.queryClient.ensureQueryData(
postBySlugQuery(params.slug),
);
if (!post) throw notFound();
return post;
},
head: ({ loaderData: post }) => ({
meta: [{ title: post?.title }],
}),
component: PostPage,
});
Key Points:
ensureQueryData(): Fetches and caches if not presentprefetchQuery()/prefetchInfiniteQuery(): For background prefetchingloaderData: Available inheadfunction for dynamic SEO
3. Component Data Access
Use useSuspenseQuery or useSuspenseInfiniteQuery in components. Since loaders prefetch, this is typically synchronous:
function PostComponent() {
const { slug } = Route.useParams();
const { data: post } = useSuspenseQuery(postBySlugQuery(slug));
return <article>{post.content}</article>;
}
4. Infinite Scroll Pattern
Use IntersectionObserver to observe a bottom sentinel element:
function PostList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useSuspenseInfiniteQuery(postsInfiniteQuery());
const sentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 0.1 }
);
if (sentinelRef.current) observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
return (
<>
{data.pages.flatMap((page) => page.posts.map((post) => (
<PostCard key={post.id} post={post} />
)))}
<div ref={sentinelRef} className="h-4" />
{isFetchingNextPage && <LoadingSpinner />}
</>
);
}
Skeleton / Pending States
Define pendingComponent for critical routes to show skeletons during client navigation:
export const Route = createFileRoute("/_public/post/$slug")({
pendingComponent: PostSkeleton,
// ...
});
function PostSkeleton() {
return (
<div className="animate-pulse">
<div className="h-8 bg-muted rounded w-3/4 mb-4" />
<div className="h-4 bg-muted rounded w-full mb-2" />
<div className="h-4 bg-muted rounded w-5/6" />
</div>
);
}
Component Organization
src/components/ Directory
| Subdirectory | Purpose |
|---|---|
ui/ | Atomic UI components (Button, Input, Card). Use cva for variants. |
common/ | Shared business components (ThemeProvider, LoadingFallback) |
layout/ | Layout components (Header, Footer, Sidebar) |
Feature Components
Components specific to a feature belong in features/<name>/components/.
Custom Hooks (src/hooks/)
Reusable hooks for cross-feature functionality:
| Hook | Purpose |
|---|---|
use-debounce.ts | Generic debouncing |
use-media-query.ts | Responsive media queries |
use-navigate-back.ts | Navigation with fallback |
use-active-toc.ts | Table of contents active state |
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Components & Hooks | kebab-case files | post-item.tsx, use-debounce.ts |
| React Components | PascalCase | PostItem |
| Variables & Functions | camelCase | getPosts |
| Types & Interfaces | PascalCase | PostItemProps |
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です