# frontend-development > 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. - Author: Di Wusi - Repository: Determina54/flare-stack-blog - Version: 20260208115916 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-08 - Source: https://github.com/Determina54/flare-stack-blog - Web: https://mule.run/skillshub/@@Determina54/flare-stack-blog~frontend-development:20260208115916 --- --- 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//queries/index.ts`. Use a **Query Key Factory** pattern to centralize and type-safe your cache keys. ```typescript // 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 ```typescript // 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/.tsx`) Use loader functions with `ensureQueryData()` or `prefetchQuery()` for SSR: ```typescript // $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: ```typescript function PostComponent() { const { slug } = Route.useParams(); const { data: post } = useSuspenseQuery(postBySlugQuery(slug)); return
{post.content}
; } ``` ### 4. Infinite Scroll Pattern Use `IntersectionObserver` to observe a bottom sentinel element: ```typescript function PostList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useSuspenseInfiniteQuery(postsInfiniteQuery()); const sentinelRef = useRef(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) => ( )))}
{isFetchingNextPage && } ); } ``` ## Skeleton / Pending States Define `pendingComponent` for critical routes to show skeletons during client navigation: ```typescript export const Route = createFileRoute("/_public/post/$slug")({ pendingComponent: PostSkeleton, // ... }); function PostSkeleton() { return (
); } ``` ## 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//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` |