Loading...
Loading...
Modern React data fetching patterns. Use when implementing caching, deduplication, optimistic updates, or parallel loading with TanStack Query, SWR, or Suspense.
npx skill4agent add patternsdev/skills react-data-fetchinguseEffectfetchPromise.allasync function loadDashboard() {
const user = await fetchUser()
const posts = await fetchPosts()
const notifications = await fetchNotifications()
return { user, posts, notifications }
}async function loadDashboard() {
const [user, posts, notifications] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchNotifications(),
])
return { user, posts, notifications }
}async function loadPage() {
const userPromise = fetchUser()
const configPromise = fetchConfig()
const user = await userPromise
const [config, posts] = await Promise.all([
configPromise,
fetchPosts(user.id), // depends on user
])
return { user, config, posts }
}awaitawaitasync function loadProfile(userId: string) {
const user = await fetchUser(userId) // waits here
const prefs = await fetchPreferences() // starts only after user resolves
const avatar = buildAvatarUrl(user.avatar)
return { user, prefs, avatar }
}async function loadProfile(userId: string) {
const userPromise = fetchUser(userId) // starts immediately
const prefsPromise = fetchPreferences() // starts immediately
const user = await userPromise // await when needed
const avatar = buildAvatarUrl(user.avatar)
const prefs = await prefsPromise // may already be resolved
return { user, prefs, avatar }
}Promise.allPromise.alluseEffectfetchfunction UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.finally(() => setLoading(false))
}, [userId])
if (loading) return <Skeleton />
return <div>{user?.name}</div>
}import { useQuery } from '@tanstack/react-query'
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
})
if (isLoading) return <Skeleton />
return <div>{user?.name}</div>
}useSuspenseQuery// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000, // 1 minute
retry: 2,
},
},
})
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)isLoadingfunction Dashboard() {
const { data: user, isLoading: userLoading } = useQuery(userQuery)
const { data: stats, isLoading: statsLoading } = useQuery(statsQuery)
if (userLoading || statsLoading) return <FullPageSpinner />
return (
<div>
<UserHeader user={user} />
<StatsPanel stats={stats} />
</div>
)
}function Dashboard() {
return (
<Suspense fallback={<FullPageSpinner />}>
<DashboardContent />
</Suspense>
)
}
function DashboardContent() {
const { data: user } = useSuspenseQuery(userQuery)
const { data: stats } = useSuspenseQuery(statsQuery)
return (
<div>
<UserHeader user={user} />
<StatsPanel stats={stats} />
</div>
)
}function Dashboard() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<UserHeader />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
</div>
)
}useSuspenseQuery{ suspense: true }import { useQueryClient } from '@tanstack/react-query'
function ProjectLink({ projectId }: { projectId: string }) {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['project', projectId],
queryFn: () => fetchProject(projectId),
staleTime: 30_000,
})
}
return (
<Link
to={`/projects/${projectId}`}
onMouseEnter={prefetch}
onFocus={prefetch}
>
View Project
</Link>
)
}// routes.tsx
const routes = [
{
path: '/projects/:id',
loader: ({ params }) => queryClient.ensureQueryData({
queryKey: ['project', params.id],
queryFn: () => fetchProject(params.id!),
}),
Component: ProjectPage,
},
]React.cache()React.cache()import { cache } from 'react'
export const getSession = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return session
})
export const getUser = cache(async (userId: string) => {
return await db.user.findUnique({ where: { id: userId } })
})getSession()// Cache miss every time — new object reference
getUser({ id: '123' })
getUser({ id: '123' }) // miss
// Cache hit — same string value
getUser('123')
getUser('123') // hitimport { useMutation, useQueryClient } from '@tanstack/react-query'
function LikeButton({ postId }: { postId: string }) {
const queryClient = useQueryClient()
const { mutate: toggleLike } = useMutation({
mutationFn: () => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ['post', postId] })
const previous = queryClient.getQueryData<Post>(['post', postId])
queryClient.setQueryData<Post>(['post', postId], old => ({
...old!,
liked: !old!.liked,
likeCount: old!.liked ? old!.likeCount - 1 : old!.likeCount + 1,
}))
return { previous }
},
onError: (_err, _vars, context) => {
queryClient.setQueryData(['post', postId], context?.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['post', postId] })
},
})
return <button onClick={() => toggleLike()}>Like</button>
}function UserPage({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
if (!user) return <Skeleton />
return <UserPosts userId={user.id} /> // starts fetching only after user loads
}
function UserPosts({ userId }: { userId: string }) {
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
})
// ...
}function UserPage({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
})
if (!user) return <Skeleton />
return (
<div>
<UserHeader user={user} />
<PostList posts={posts ?? []} />
</div>
)
}// hooks/useOnlineStatus.ts
import { useSyncExternalStore } from 'react'
function subscribe(callback: () => void) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
function getSnapshot() {
return navigator.onLine
}
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, () => true)
}useSyncExternalStorepreventDefault()useEffect(() => {
const handler = () => trackScroll(window.scrollY)
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [])useEffect(() => {
const handler = () => trackScroll(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])const [prefs, setPrefs] = useState(() => {
return JSON.parse(localStorage.getItem('prefs') || '{}')
})const PREFS_VERSION = 2
const [prefs, setPrefs] = useState<Prefs>(() => {
try {
const raw = localStorage.getItem('prefs')
if (!raw) return DEFAULT_PREFS
const parsed = JSON.parse(raw)
if (parsed._v !== PREFS_VERSION) return DEFAULT_PREFS
return parsed
} catch {
return DEFAULT_PREFS
}
})
// On save, include version
useEffect(() => {
localStorage.setItem('prefs', JSON.stringify({ ...prefs, _v: PREFS_VERSION }))
}, [prefs])