Back to list
du2333

frontend-development

by du2333

全栈个人博客,由Cloudflare全家桶驱动

1🍴 0📅 Jan 23, 2026

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 present
  • prefetchQuery() / prefetchInfiniteQuery(): For background prefetching
  • loaderData: Available in head function 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

SubdirectoryPurpose
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:

HookPurpose
use-debounce.tsGeneric debouncing
use-media-query.tsResponsive media queries
use-navigate-back.tsNavigation with fallback
use-active-toc.tsTable of contents active state

Naming Conventions

TypeConventionExample
Components & Hookskebab-case filespost-item.tsx, use-debounce.ts
React ComponentsPascalCasePostItem
Variables & FunctionscamelCasegetPosts
Types & InterfacesPascalCasePostItemProps

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon