算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
frontend-data-fetching【设计与创意】Use this skill when the user says 'data fetching', 'TanStack Query', 'SWR', 'React Query', 'server state', 'API client', 'data fetching pattern', 'cache invalidation', 'optimistic update', 'pagination data', 'infinite scroll', 'stale-while-revalidate'. Design data fetching layer for frontend apps. Do NOT use for: backend API design or database querie…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Manage server state efficiently on the client — eliminating boilerplate, providing caching, deduplication, background refetching, and optimistic mutations while keeping server state out of global client stores.
User request includes any of: "data fetching", "TanStack Query", "SWR", "React Query", "server state", "API client", "data fetching pattern", "cache invalidation", "optimistic update", "pagination data", "infinite scroll", "stale-while-revalidate".
Data fetching architecture with query/mutation patterns and caching strategy.
## Strategy
<library, cache-key-design, staleTime>
## Query Layer
<query-hooks, pagination, revalidation>
## Mutation Layer
<optimistic-updates, invalidation, error-handling>
## Cache Config
<staleTime, cacheTime, persistence>
—
Compression footer: frontend-data-fetching/v1 | 4 sections | lib: <selected> | cache: <persisted|memory>
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
4096 tokens
Project type?
|-- Complex app with mutations, pagination, optimistic updates?
| |-- YES --> TanStack Query v5
| |-- NO --> Read-heavy, minimal mutations?
| |-- YES --> SWR
| |-- NO --> Using Redux already? --> RTK Query
|-- GraphQL API?
|-- YES --> Apollo Client or urql
|-- NO --> tRPC available? --> tRPC client
Consider: bundle size (~13KB TanStack vs ~4KB SWR), framework support,
devtools quality, cache persistence requirements.
Data volatility?
|-- Real-time (chat, notifications) -->
| |-- Polling: refetchInterval: 5000
| |-- WebSocket: integrate with queryClient.setQueryData
|-- Frequently changing (dashboard, feed) -->
| |-- staleTime: 30s, refetchOnWindowFocus: true
|-- Rarely changing (config, reference data) -->
| |-- staleTime: 5min, gcTime: 30min
|-- Never changes (static content) -->
|-- staleTime: Infinity (fetch once, never refetch)
Query error received?
|-- Network error (no response) -->
| |-- Show stale data if available
| |-- Show offline message if no stale data
|-- 4xx (client error) -->
| |-- Do NOT retry (client mistake)
| |-- Show validation error or permission denied
|-- 5xx (server error) -->
|-- Retry with exponential backoff (default 3 times)
|-- Show error state after all retries exhausted
refetchInterval.// TanStack Query v5 — basic setup
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // 30s before considered stale
gcTime: 5 * 60_000, // 5min unused data stays in cache
retry: 3, // retry 3 times with backoff
refetchOnWindowFocus: true, // refetch when user returns
refetchOnReconnect: true, // refetch on network recovery
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<AppContent />
</QueryClientProvider>
)
}
onMutate, onError, onSettled.// Optimistic update pattern
function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (newTodo: Todo) => api.post('/todos', newTodo),
onMutate: async (newTodo) => {
// Cancel outgoing refetches to avoid overwriting optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot previous value for rollback
const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])
// Optimistically update cache
queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [...old, newTodo])
return { previousTodos }
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context?.previousTodos)
showToast('Failed to add todo', 'error')
},
onSettled: () => {
// Always refetch to ensure server consistency
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
useQuery with page param, prefetch next page.useInfiniteQuery with getNextPageParam.fetchNextPage.isFetchingNextPage vs isLoading.// Infinite scroll with cursor pagination
function useInfiniteProducts() {
return useInfiniteQuery({
queryKey: ['products'],
queryFn: ({ pageParam }) => api.getProducts({ cursor: pageParam }),
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? null,
})
}
function ProductList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteProducts()
const observerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!observerRef.current) return
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage) fetchNextPage()
},
{ threshold: 0.5 }
)
observer.observe(observerRef.current)
return () => observer.disconnect()
}, [hasNextPage, fetchNextPage])
return (
<div>
{data?.pages.map(page => page.items.map(item => <ProductCard key={item.id} item={item} />))}
{isFetchingNextPage && <Spinner />}
<div ref={observerRef} />
</div>
)
}
QueryCache.onError / MutationCache.onError.refetchOnReconnect: true.// Global error handling
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
if (error instanceof AuthenticationError) {
// Redirect to login
window.location.href = '/login'
}
console.error(`Query ${query.queryKey} failed:`, error)
},
}),
mutationCache: new MutationCache({
onError: (error) => {
showToast(error.message, 'error')
},
}),
})
staleTime: how long data is considered fresh (default 0). Set per query based on update frequency.gcTime (v5) / cacheTime (v4): how long unused data stays in cache (default 5 min).// Per-query staleTime configuration
const useUser = (id: string) => useQuery({
queryKey: ['users', id],
queryFn: () => api.getUser(id),
staleTime: 5 * 60_000, // user data fresh for 5min
gcTime: 30 * 60_000, // keep in cache for 30min after unmount
})
const useStockPrice = (symbol: string) => useQuery({
queryKey: ['stocks', symbol],
queryFn: () => api.getStockPrice(symbol),
staleTime: 10_000, // 10 seconds
refetchInterval: 30_000, // poll every 30s
})
// Hierarchical key structure
['todos'] // All todos
['todos', todoId] // Single todo
['todos', { status: 'done' }] // Filtered todos
['todos', todoId, 'comments'] // Comments on a todo
['users', userId, 'posts', postFilters] // Nested resources
// Prefetch on hover
function ProductLink({ id }: { id: string }) {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['products', id],
queryFn: () => api.getProduct(id),
staleTime: 60_000,
})
}
return (
<Link to={`/products/${id}`} onMouseEnter={prefetch} onFocus={prefetch}>
View Product
</Link>
)
}
// Prefetch next page
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchInfiniteQuery({
queryKey: ['products'],
pages: 1,
})
}
}, [hasNextPage, queryClient])
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import { persistQueryClient } from '@tanstack/react-query-persist-client'
const persister = createSyncStoragePersister({
storage: window.localStorage,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
})
persistQueryClient({
queryClient,
persister,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
return query.queryKey[0] !== 'sensitive-data' // Don't persist sensitive data
},
},
})
Is the data from a server?
No -> Use client state (Zustand, Context, Redux)
Yes -> Does it need caching/deduplication?
No -> Plain fetch in useEffect
Yes -> Is app complex with mutations?
Yes -> TanStack Query
No -> SWR
Is the data user-specific?
Yes -> Include userId in query key
No -> Global cache key, longer staleTime
Does the data need to update in real-time?
Yes -> Polling or WebSocket integration
No -> Stale-while-revalidate
| Feature | TanStack Query v5 | SWR | RTK Query |
|---|---|---|---|
| Bundle size | ~13KB | ~4KB | ~12KB (with Redux) |
| Pagination | useInfiniteQuery | useSWRInfinite | endpoints with pagination |
| Optimistic updates | onMutate + rollback | mutate with revalidate | onQueryStarted |
| Devtools | Rich UI | Basic | Redux DevTools |
| Cache persistence | @tanstack/query-persist-client-key | LocalStorage plugin | Redux persist |
| Framework agnostic | Yes (React, Vue, Solid) | React only | Redux only |
@tanstack/react-query-devtools — visual cache inspector, query toggle, data explorer.@sentry integration — capture query failures as breadcrumbs.@tanstack/query-sync-storage-persister — persist cache to localStorage/AsyncStorage.@tanstack/query-broadcast-client-experimental — sync cache across tabs.msw (Mock Service Worker) — mock API responses for development and testing.@tanstack/react-query-network-devtools — inspect network requests for queries.staleTime reflects how fresh data needs to be — set per query, not globally.If data fetching requires complex WebSocket sync, optimistic offline queue with conflict resolution, or server-side data hydration beyond basic SSR, flag for senior engineer review. Otherwise implement complete fetching layer.
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
Is the data required for page load?
├── No → Lazy fetch with useQuery/useSWR (on interaction or viewport)
└── Yes → Is it user-specific?
├── Yes → SSR fetch in load function or getServerSideProps
└── No → Static generation (SSG) with revalidation (ISR)
Does the data change frequently?
├── Yes → SWR/stale-while-revalidate with refetch interval
└── No → Cache-First with background revalidation
Does the mutation need optimistic UI?
├── No → Standard mutation with refetch on success
└── Yes → Can you predict the response?
├── Yes → Optimistic update with rollback on error
└── No → Show loading state, replace on response
Does the mutation affect multiple queries?
├── Yes → Invalidate all affected query keys after mutation
└── No → Invalidate single query key on success