Implementing Data Fetching Patterns
1. Using React Query
Example: Fetching User Data
import { useQuery } from 'react-query';
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery(
['user', userId],
() => fetchUser(userId)
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Hello, {data.name}!</div>;
}
async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
| Property | Description | Default |
|---|---|---|
| queryKey | Unique identifier for the query | Required |
| queryFn | Function that fetches the data | Required |
| enabled | Whether the query should run | true |
| staleTime | How long data is considered fresh (ms) | 1000 * 60 * 5 |
| cacheTime | How long to keep query in cache (ms) | 1000 * 60 * 5 |
| retry | Number of retry attempts or a boolean | false |
Note: React Query automatically handles caching, deduplication, and refetching of data.
Example: Mutating Data
import { useMutation, useQueryClient } from 'react-query';
function UpdateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
// Invalidate and refetch queries
queryClient.invalidateQueries('user');
}
});
return (
<button onClick={() => mutation.mutate({ id: 1, name: 'New Name' })}>
Update User
</button>
);
}
async function updateUser({ id, name }) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
return response.json();
}
Key Features
- NEW Automatic background refetching
- NEW Smart cache invalidation
- NEW Parallel query execution
- Automatic retry with exponential backoff
- Optimistic updates for better UX
2. Implementing Query Caching
Example: Using React Query for Caching
import { useQuery } from 'react-query';
function UserProfile({ userId }) {
const { data, isLoading } = useQuery(
['user', userId],
() => fetchUser(userId),
{
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
}
);
if (isLoading) return <div>Loading...</div>;
return <div>User: {data.name}</div>;
}
| Option | Values | Description |
|---|---|---|
| staleTime | 5 * 60 * 1000 |
Time in milliseconds after which data is considered stale |
| cacheTime | 10 * 60 * 1000 |
Time in milliseconds after which data is removed from cache |
| refetchOnWindowFocus | true |
Whether to refetch when window regains focus |
Note: React Query automatically handles cache invalidation and re-fetching based on these settings.
Comparison: Caching Strategies
- Cache-First: Serve from cache if available, fetch in background
- Stale-While-Revalidate: Serve stale cache while fetching fresh data
- Cache-And-Refresh: Fetch new data and update cache immediately
3. Using Mutations
| Property | Description | Example |
|---|---|---|
| mutationFn | Function that performs the mutation | async () => { await fetch('/api/users', { method: 'POST', body: JSON.stringify(userData) }) } |
| onSuccess | Callback when mutation succeeds | (data) => console.log('User created:', data) |
| onError | Callback when mutation fails | (error) => console.error('Failed to create user:', error) |
Example: Creating a User with Mutation
import { useMutation } from 'react-native-queries';
const createUser = async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
return response.json();
};
const { mutate, isLoading, isError } = useMutation({
mutationFn: createUser,
onSuccess: (data) => {
console.log('User created:', data);
},
onError: (error) => {
console.error('Failed to create user:', error);
}
});
// Usage
mutate({ name: 'John Doe', email: 'john@example.com' });
Note: Mutations in React Native are typically used for creating, updating, or deleting data on a server.
4. Handling Loading and Error States
Example: Basic Loading and Error State Handling
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>Hello, {user.name}!</div>;
}
| State | Description | Best Practice |
|---|---|---|
| loading | Indicates whether data is being fetched | Show a skeleton or spinner |
| error | Contains error message if fetch failed | Display user-friendly error with retry option |
Note: Always reset error state when retrying a fetch.
Loading vs Error States Comparison
- Loading: Show progress indicator, disable UI elements
- Error: Show error message, provide retry mechanism
Recommended Loading State Workflow
- Set loading to true when fetch begins
- Render loading UI (spinner, skeleton)
- Fetch completes or fails
- Set loading to false
- Handle success or error accordingly
5. Implementing Optimistic Updates
Optimistic updates are a technique used to improve perceived performance by immediately updating the UI with the expected result of a user action, before the actual server response is received.
| Property | Description | Usage |
|---|---|---|
| optimisticData | Data to be displayed immediately before server response | Passed to update function in state management |
| rollbackData | Original data to revert to if server fails | Used in error handling logic |
Example: Basic Optimistic Update in React Native
const [posts, setPosts] = useState([]);\n\nconst handleLike = async (postId) => {\n const postIndex = posts.findIndex(p => p.id === postId);\n const currentPost = posts[postIndex];\n\n // Optimistically update UI\n const updatedPosts = [...posts];\n updatedPosts[postIndex] = {\n ...currentPost,\n likes: currentPost.likes + 1\n };\n setPosts(updatedPosts);\n\n try {\n await api.likePost(postId);\n } catch (error) {\n // Revert on failure\n setPosts([...posts]);\n }\n};
Note: Ensure that the optimistic update matches the expected server response format to avoid UI inconsistencies.
Warning: If the server response differs from the optimistic update, manual reconciliation may be required.
Key Considerations
- Always validate that optimistic updates align with server-side logic
- Implement rollback mechanisms for failed requests
- Use consistent data structures between client and server
6. Using SWR for Data Fetching
Example: Basic SWR Usage
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
function Profile() {
const { data, error } = useSWR('/api/user', fetcher);
if (error) return <div>Failed to load</div>;
if (!data) return <div>Loading...</div>;
return <div>Hello {data.name}!</div>;
}
| Property | Description | Default |
|---|---|---|
| key | Unique identifier for the cache entry | Required |
| fetcher | Function to fetch data from the API | Required |
| config | Configuration options like revalidation, retry, etc. | Optional |
Note: SWR provides automatic caching, revalidation, and error handling out of the box.
Key Features
- Automatic caching and revalidation
- Stale-while-revalidate strategy
- Configurable retry logic
- Supports both client and server-side rendering
7. Implementing Infinite Queries
Example: Basic Infinite Scroll with React Query
import { useInfiniteQuery } from 'react-query';
const fetchPosts = async ({ pageParam = 1 }) => {
const response = await fetch(`/api/posts?page=${pageParam}&limit=10`);
return response.json();
};
function PostList() {
const { data, fetchNextPage, hasNextPage, isLoading } = useInfiniteQuery(
'posts',
fetchPosts,
{
getNextPageParam: (lastPage) => lastPage.nextPage,
}
);
if (isLoading) return <div>Loading...</div>;
const posts = data.pages.flatMap(page => page.posts);
return (
<>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>Load More</button>
)}
<>
);
}
| Property | Description | Default |
|---|---|---|
| getNextPageParam | Function to determine the next page parameter | undefined |
| getPreviousPageParam | Function to determine the previous page parameter | undefined |
| initialPageParam | Initial page parameter value | undefined |
Note: The
getNextPageParam function should return the next page parameter or undefined if there are no more pages.
Warning: Make sure to handle loading states and error states properly when implementing infinite queries.
Key Points
- Infinite queries are useful for loading large datasets in chunks.
- Use
getNextPageParamto determine when to fetch the next page. - React Query handles caching and deduplication automatically.
8. Using Query Invalidation
Example: Invalidating a Query After Mutation
import { useQueryClient } from 'react-query';
function UpdateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
// Invalidate the 'users' query to refetch
queryClient.invalidateQueries('users');
}
});
}
| Method | Description | Usage |
|---|---|---|
| invalidateQueries | Invalidates one or more queries by key or predicate | queryClient.invalidateQueries('users') |
| refetchQueries | Invalidates and refetches queries | queryClient.refetchQueries('users') |
Note: Invalidating queries is useful when data has changed on the server and needs to be refreshed in the client.
9. Handling Background Refetching
| Property | Description | Default |
|---|---|---|
| refetchOnMount | Refetch when component mounts | false |
| refetchOnReconnect | Refetch when network reconnects | true |
| refetchOnWindowFocus | Refetch when window regains focus | true |
| refetchInterval | Refetch interval in milliseconds | false |
Example: Configure background refetching
const { data, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnMount: true,
refetchOnReconnect: true,
refetchOnWindowFocus: false,
refetchInterval: 30000
})
Note: Background refetching is ideal for data that changes infrequently but should stay updated in the UI.
10. Implementing Prefetching
Example: Prefetching with React Query
import { useQuery } from 'react-query';
const fetchUser = async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
};
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery(
['user', userId],
() => fetchUser(userId),
{
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
}
);
return <div>{user?.name}</div>;
}
function UserList() {
const { data: users } = useQuery(
['users'],
() => fetchUsers(),
{
staleTime: 5 * 60 * 1000,
cacheTime: 10 * 60 * 1000,
// Prefetch data for the next screen
refetchOnMount: false,
}
);
return <div></div>;
}
| Property | Description | Default Value |
|---|---|---|
| staleTime | Time in milliseconds after which data is considered stale and needs refetching | 1000 * 60 * 5 (5 minutes) |
| cacheTime | Time in milliseconds after which data is removed from cache | 1000 * 60 * 10 (10 minutes) |
| refetchOnMount | Whether to refetch data when component mounts | true |
Note: Prefetching can be done using
queryClient.prefetchQuery for more control over when and how data is prefetched.