Skip to main content

declarative-queries

SuspenseQuery, SuspenseQueries, SuspenseInfiniteQuery render-prop components for fetching in JSX without hook wrapper components. Load when placing queries at the same depth as Suspense/ErrorBoundary, running parallel queries, using select, integrating queryOptions/infiniteQueryOptions, or replacing useSuspenseQuery wrapper components.

소스 정보

저장소
toss/suspensive
최근 소스 활동
2026년 7월 4일 07:04
감지된 SKILL.md 언어
영어
스타
1,059
포크
99

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
declarative-queries
description
SuspenseQuery, SuspenseQueries, SuspenseInfiniteQuery render-prop components for fetching in JSX without hook wrapper components. Load when placing queries at the same depth as Suspense/ErrorBoundary, running parallel queries, using select, integrating queryOptions/infiniteQueryOptions, or replacing useSuspenseQuery wrapper components.
metadata
{"type":"sub-skill","library":"@suspensive/react-query-4","library_version":"3.21.2","framework":"react"}
requires
["react-query"]
sources
["toss/suspensive:docs/suspensive.org/src/content/en/docs/react-query/SuspenseQuery.mdx","toss/suspensive:docs/suspensive.org/src/content/en/docs/react-query/SuspenseQueries.mdx","toss/suspensive:docs/suspensive.org/src/content/en/docs/react-query/SuspenseInfiniteQuery.mdx","toss/suspensive:docs/suspensive.org/src/content/en/docs/react-query/motivation.mdx"]
This skill builds on react-query. Read ../SKILL.md first. SuspenseQuery/SuspenseQueries/SuspenseInfiniteQuery move data fetching into JSX so what suspends is visible at the same depth as the Suspense and ErrorBoundary handling it, and child components stay presentational. ## Setup ```bash npm install @suspensive/react-query-4 @suspensive/react @tanstack/react-query@4 ``` Components come from '@suspensive/react-query-4'; queryOptions/infiniteQueryOptions come from '@tanstack/react-query' (official in TSQ 4.40+/4.41+; the Suspensive re-exports are deprecated). ## Core Patterns ### Parallel queries at one depth with SuspenseQuery ```tsx 'use client' import { ErrorBoundary, Suspense } from '@suspensive/react' import { SuspenseQuery } from '@suspensive/react-query-4' import { PostListItem, UserProfile } from '~/components' import { postsQueryOptions, userQueryOptions } from '~/queries' export const PostsPage = ({ userId }: { userId: number }) => ( <ErrorBoundary fallback={({ error }) => <>{error.message}</>}> <Suspense fallback={'loading...'}> <SuspenseQuery {...userQueryOptions(userId)}> {({ data: user }) => <UserProfile key={user.id} {...user} />} </SuspenseQuery> <SuspenseQuery {...postsQueryOptions(userId)} select={(posts) => posts.filter(({ isPublic }) => isPublic)}> {({ data: posts }) => posts.map((post) => <PostListItem key={post.id} {...post} />)} </SuspenseQuery> </Suspense> </ErrorBoundary> ) ``` Both queries fetch in parallel; the boundary scope is obvious because both sources of suspension sit directly under `Suspense`. ### Combined results with SuspenseQueries ```tsx 'use client' import { ErrorBoundary, Suspense } from '@suspensive/react' import { SuspenseQueries } from '@suspensive/react-query-4' import { PostListItem, UserProfile } from '~/components' import { postsQueryOptions, userQueryOptions } from '~/queries' export const PostsPage = ({ userId }: { userId: number }) => ( <ErrorBoundary fallback={({ error }) => <>{error.message}</>}> <Suspense fallback={'loading...'}> <SuspenseQueries queries={[userQueryOptions(userId), postsQueryOptions(userId)]}> {([{ data: user }, { data: posts }]) => ( <> <UserProfile {...user} /> {posts.map((post) => ( <PostListItem key={post.id} {...post} /> ))} </> )} </SuspenseQueries> </Suspense> </ErrorBoundary> ) ``` Use SuspenseQueries when one render needs both results together (useSuspenseQueries in JSX). ### Infinite scrolling with SuspenseInfiniteQuery TSQ v4 infinite queries take no `initialPageParam` — default the cursor in the queryFn signature instead. ```tsx 'use client' import { ErrorBoundary, Suspense } from '@suspensive/react' import { SuspenseInfiniteQuery } from '@suspensive/react-query-4' import { infiniteQueryOptions } from '@tanstack/react-query' import { PostListItem } from '~/components' import { getPosts } from '~/api' const postsInfiniteQueryOptions = (userId: number) => infiniteQueryOptions({ queryKey: ['users', userId, 'posts'], queryFn: ({ pageParam = 0 }) => getPosts(userId, pageParam), getNextPageParam: (lastPage) => lastPage.nextCursor, }) export const InfinitePostsPage = ({ userId }: { userId: number }) => ( <ErrorBoundary fallback={({ error }) => <>{error.message}</>}> <Suspense fallback={'loading...'}> <SuspenseInfiniteQuery {...postsInfiniteQueryOptions(userId)}> {({ data, fetchNextPage, hasNextPage }) => ( <> {data.pages.flatMap((page) => page.posts.map((post) => <PostListItem key={post.id} {...post} />))} <button type="button" disabled={!hasNextPage} onClick={() => fetchNextPage()}> Load More </button> </> )} </SuspenseInfiniteQuery> </Suspense> </ErrorBoundary> ) ``` ## Common Mistakes ### [MEDIUM] Wrapper components created only to call useSuspenseQuery Wrong: ```tsx const UserInfo = ({ userId }: { userId: number }) => { const { data } = useSuspenseQuery(userQueryOptions(userId)) return <UserProfile {...data} /> } ``` Correct: ```tsx <SuspenseQuery {...userQueryOptions(userId)}>{({ data }) => <UserProfile {...data} />}</SuspenseQuery> ``` Hooks force an extra child component under Suspense whose name hides that it suspends; SuspenseQuery keeps fetching visible in JSX so only presentational components remain. Source: docs/suspensive.org/src/content/en/docs/react-query/motivation.mdx ### [HIGH] SuspenseQuery without an ancestor Suspense Wrong: ```tsx <SuspenseQuery {...postQueryOptions(id)}>{({ data }) => <Post data={data} />}</SuspenseQuery> ``` Correct: ```tsx <Suspense fallback={<PostSkeleton />}> <SuspenseQuery {...postQueryOptions(id)}>{({ data }) => <Post data={data} />}</SuspenseQuery> </Suspense> ``` The component suspends; with no boundary above it, suspension bubbles to the nearest ancestor Suspense or the app root, blanking unrelated UI. Source: https://github.com/toss/suspensive/issues/1654 ### [HIGH] Rendering SuspenseQuery in a React Server Component Wrong: ```tsx // app/page.tsx — Server Component import { SuspenseQuery } from '@suspensive/react-query-4' export default function Page() { return ( <SuspenseQuery queryKey={['posts']} queryFn={getPosts}> {({ data }) => <PostList posts={data} />} </SuspenseQuery> ) } ``` Correct: ```tsx 'use client' import { SuspenseQuery } from '@suspensive/react-query-4' export const Posts = () => ( <SuspenseQuery queryKey={['posts']} queryFn={getPosts}> {({ data }) => <PostList posts={data} />} </SuspenseQuery> ) ``` Render-prop children are functions and cannot cross the RSC serialization boundary; keep these components inside 'use client' modules. Source: https://github.com/toss/suspensive/issues/1563 ### [HIGH] Passing enabled or placeholderData to suspense queries Wrong: ```tsx <SuspenseQuery queryKey={['user', userId]} queryFn={getUser} enabled={!!userId}> {({ data }) => <UserProfile {...data} />} </SuspenseQuery> ``` Correct: ```tsx { userId ? ( <SuspenseQuery queryKey={['user', userId]} queryFn={() => getUser(userId)}> {({ data }) => <UserProfile {...data} />} </SuspenseQuery> ) : null } ``` Suspense queries must guarantee data, so enabled/placeholderData are excluded from their option types — express conditionality with JSX instead. Source: docs/suspensive.org/src/content/en/docs/react-query/migration (v2 breaking changes) ### [MEDIUM] Checking isLoading/isError on suspense query results Wrong: ```tsx <SuspenseQuery {...postsQueryOptions()}> {({ data, isLoading }) => (isLoading ? <Spinner /> : <List data={data} />)} </SuspenseQuery> ``` Correct: ```tsx <SuspenseQuery {...postsQueryOptions()}>{({ data }) => <List data={data} />}</SuspenseQuery> ``` Suspense query result types have no loading/error branches — `data` is always the success type because Suspense and ErrorBoundary guarantee it, so guards are dead code. Source: docs/suspensive.org/src/content/en/docs/react-query/motivation.mdx See also: ../mutations/SKILL.md for Mutation in list rows, ../prefetching/SKILL.md to warm the cache before these components suspend, ../../compositions/suspensive-react/SKILL.md for ErrorBoundary reset wiring around SuspenseQuery.
GitHub에서 보기