React Query এর Powerful Features - একটি Complete Guide
React application development এ server state management একটা বিশাল চ্যালেঞ্জ। আর এই সমস্যার সমাধানে React Query (এখন TanStack Query নামে পরিচিত) একটা অসাধারণ library। আজকে আমরা React Query এর কিছু important features নিয়ে বিস্তারিত আলোচনা করব।
Infinite Queries - Endless Scrolling এর জন্য
আপনি কি কখনো Facebook বা Instagram এর মতো infinite scroll দেখেছেন? যেখানে scroll করলে নতুন নতুন post load হতে থাকে? React Query এর useInfiniteQuery দিয়ে এটা খুব সহজেই করা যায়।
import { useInfiniteQuery } from '@tanstack/react-query'
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length + 1 : undefined
}
})
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>
{isFetchingNextPage ? 'Loading...' : 'আরো দেখুন'}
</button>
)}
</div>
)
}
এখানে getNextPageParam function টি বলে দেয় next page এর parameter কি হবে। খুবই simple, তাই না?
Parameterized Queries - Dynamic Data Fetching
অনেক সময় আমাদের query parameter এর উপর ভিত্তি করে data fetch করতে হয়। যেমন একটি specific user এর profile দেখানো বা কোনো particular product এর details।
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId // userId থাকলেই query চালু হবে
})
if (isLoading) return <div>User info loading...</div>
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
}
// অন্য component থেকে
function App() {
const [selectedUserId, setSelectedUserId] = useState(null)
return (
<div>
<button onClick={() => setSelectedUserId(1)}>User 1</button>
<button onClick={() => setSelectedUserId(2)}>User 2</button>
{selectedUserId && <UserProfile userId={selectedUserId} />}
</div>
)
}
queryKey তে userId দেওয়ার কারণে প্রতিটি different user এর জন্য আলাদা cache তৈরি হবে।
Custom Queries - নিজস্ব Query Hook
বারবার একই ধরনের query লিখতে boring লাগে? Custom hook বানিয়ে ফেলুন!
// hooks/useUser.js
function useUser(userId) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 3,
enabled: !!userId
})
}
// hooks/usePosts.js
function usePosts(filters = {}) {
return useQuery({
queryKey: ['posts', filters],
queryFn: () => fetchPosts(filters),
select: (data) => data.filter(post => post.published) // শুধু published posts
})
}
// এখন যেকোনো component এ use করুন
function Profile({ userId }) {
const { data: user, isLoading } = useUser(userId)
const { data: posts } = usePosts({ authorId: userId })
// ...
}
Query Key Factory - Smart Key Management
বড় application এ query key গুলো organize করা জরুরি। Query Key Factory pattern use করে এটা clean ভাবে করা যায়।
// utils/queryKeys.js
export const queryKeys = {
users: {
all: ['users'],
lists: () => [...queryKeys.users.all, 'list'],
list: (filters) => [...queryKeys.users.lists(), filters],
details: () => [...queryKeys.users.all, 'detail'],
detail: (id) => [...queryKeys.users.details(), id],
},
posts: {
all: ['posts'],
lists: () => [...queryKeys.posts.all, 'list'],
list: (filters) => [...queryKeys.posts.lists(), filters],
detail: (id) => [...queryKeys.posts.all, 'detail', id],
}
}
// hooks/useUser.js
function useUser(userId) {
return useQuery({
queryKey: queryKeys.users.detail(userId),
queryFn: () => fetchUser(userId)
})
}
// এখন invalidation খুব easy
function deleteUser(userId) {
// specific user এর cache clear করা
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(userId) })
// সব user list refresh করা
queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() })
}
Query Invalidation - Cache Refresh করা
Data update হওয়ার পর cache outdated হয়ে যায়। Query invalidation দিয়ে এটা fix করা যায়।
function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createPost,
onSuccess: () => {
// নতুন post create এর পর সব post list refresh করো
queryClient.invalidateQueries({ queryKey: ['posts'] })
// specific user এর post list refresh করো
queryClient.invalidateQueries({
queryKey: ['posts'],
predicate: (query) => query.queryKey.includes('user-posts')
})
}
})
}
function CreatePostForm() {
const createPost = useCreatePost()
const handleSubmit = (formData) => {
createPost.mutate(formData)
}
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit" disabled={createPost.isLoading}>
{createPost.isLoading ? 'Creating...' : 'Post করুন'}
</button>
</form>
)
}
Optimistic Updates - Instant UI Update
User experience বাড়ানোর জন্য optimistic update use করা হয়। এতে server response এর জন্য wait না করেই UI update হয়ে যায়।
function useUpdatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updatePost,
onMutate: async (updatedPost) => {
// ongoing queries cancel করো
await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] })
// previous data backup রাখো
const previousPost = queryClient.getQueryData(['posts', updatedPost.id])
// new data দিয়ে optimistically update করো
queryClient.setQueryData(['posts', updatedPost.id], updatedPost)
return { previousPost, updatedPost }
},
onError: (err, updatedPost, context) => {
// error হলে previous data restore করো
if (context?.previousPost) {
queryClient.setQueryData(
['posts', updatedPost.id],
context.previousPost
)
}
},
onSettled: (data, error, updatedPost) => {
// যাই হোক, query refresh করো
queryClient.invalidateQueries({ queryKey: ['posts', updatedPost.id] })
}
})
}
Simple Mutations - Basic CRUD Operations
Common CRUD operations এর জন্য mutation use করা হয়।
function useDeletePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: deletePost,
onSuccess: (deletedPost) => {
// specific post cache থেকে remove করো
queryClient.removeQueries({ queryKey: ['posts', deletedPost.id] })
// post lists update করো
queryClient.setQueryData(['posts'], (oldData) => {
return oldData?.filter(post => post.id !== deletedPost.id)
})
toast.success('Post successfully deleted!')
},
onError: (error) => {
toast.error('Post delete করতে problem হয়েছে!')
}
})
}
function PostItem({ post }) {
const deletePost = useDeletePost()
const handleDelete = () => {
if (confirm('Are you sure?')) {
deletePost.mutate(post.id)
}
}
return (
<div>
<h3>{post.title}</h3>
<p>{post.content}</p>
<button
onClick={handleDelete}
disabled={deletePost.isLoading}
>
{deletePost.isLoading ? 'Deleting...' : 'Delete করুন'}
</button>
</div>
)
}
Global Error Handling - Centralized Error Management
সব জায়গায় আলাদা আলাদা error handling না করে global setup করা যায়।
// utils/queryClient.js
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
staleTime: 5 * 60 * 1000,
onError: (error) => {
if (error.status === 401) {
// Unauthorized - login page এ redirect করো
window.location.href = '/login'
} else {
toast.error('Something went wrong!')
}
}
},
mutations: {
onError: (error) => {
if (error.status === 403) {
toast.error('আপনার এই action এর permission নেই!')
} else {
toast.error('Operation failed!')
}
}
}
}
})
// App.js
function App() {
return (
<QueryClientProvider client={queryClient}>
<ErrorBoundary fallback={<ErrorPage />}>
<Router>
{/* your app routes */}
</Router>
</ErrorBoundary>
</QueryClientProvider>
)
}
Suspense Queries - React Suspense Integration
React Suspense এর সাথে React Query use করে loading states আরো elegant করা যায়।
// components/UserProfile.js
function UserProfile({ userId }) {
const { data: user } = useSuspenseQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId)
})
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
}
// App.js
function App() {
return (
<Suspense fallback={<div>User info loading...</div>}>
<UserProfile userId={1} />
</Suspense>
)
}
Disabling Queries - Conditional Query Control
কিছু সময় আমাদের query conditionally enable/disable করতে হয়।
function UserPosts({ userId, isVisible }) {
const { data: posts } = useQuery({
queryKey: ['user-posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!userId && isVisible, // userId আছে এবং visible হলেই চালু
refetchOnWindowFocus: false, // window focus এ refetch করবে না
refetchInterval: isVisible ? 30000 : false // visible হলে 30 seconds interval এ update
})
return (
<div>
{posts?.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
Selectors - Data Transformation
API থেকে আসা raw data কে component এর জন্য required format এ transform করা যায়।
function useUserStats(userId) {
return useQuery({
queryKey: ['user-stats', userId],
queryFn: () => fetchUserData(userId),
select: (userData) => ({
totalPosts: userData.posts.length,
publishedPosts: userData.posts.filter(p => p.published).length,
averageViews: userData.posts.reduce((acc, p) => acc + p.views, 0) / userData.posts.length,
recentPosts: userData.posts
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, 5)
})
})
}
function UserDashboard({ userId }) {
const { data: stats, isLoading } = useUserStats(userId)
if (isLoading) return <div>Statistics loading...</div>
return (
<div>
<h3>আপনার Statistics</h3>
<p>Total Posts: {stats.totalPosts}</p>
<p>Published Posts: {stats.publishedPosts}</p>
<p>Average Views: {stats.averageViews.toFixed(1)}</p>
</div>
)
}
Practical Tips এবং Best Practices
1. Query Key Consistency
// ❌ Inconsistent
const userQuery = useQuery(['user', userId])
const postsQuery = useQuery(['userPosts', userId])
// ✅ Consistent
const userQuery = useQuery(['users', userId])
const postsQuery = useQuery(['users', userId, 'posts'])
2. Error Boundary Integration
function PostList() {
const { data: posts, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
throwOnError: true // Error boundary এ error throw করবে
})
return (
<div>
{posts.map(post => <PostItem key={post.id} post={post} />)}
</div>
)
}
3. Background Refetch Control
function useUserProfile(userId) {
return useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes fresh রাখো
refetchOnMount: 'always',
refetchOnWindowFocus: false,
refetchOnReconnect: 'always'
})
}
Final Thoughts
React Query এর এই features গুলো use করে আপনি complex data fetching এবং state management problems solve করতে পারবেন। Key points:
- Infinite Queries দিয়ে pagination handle করা
- Custom Hooks দিয়ে reusable logic তৈরি করা
- Query Invalidation দিয়ে fresh data ensure করা
- Optimistic Updates দিয়ে better UX provide করা
- Global Error Handling দিয়ে consistent error management
এগুলো master করলে আপনার React app অনেক বেশি performant এবং user-friendly হবে। সবচেয়ে ভাল কথা হল, এগুলো implement করতে খুব বেশি complex code লিখতে হয় না!
প্রতিটি feature নিয়ে practice করুন এবং নিজের project এ apply করার চেষ্টা করুন। React Query এর official documentation ও খুবই comprehensive, তাই আরো detail জানতে সেখানে check করতে পারেন।
