スキル一覧に戻る
du2333

frontend-development

by du2333

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

1🍴 0📅 2026年1月23日
GitHubで見るManusで実行

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

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

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

レビュー

💬

レビュー機能は近日公開予定です