Skip to main content

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
  1. Set loading to true when fetch begins
  2. Render loading UI (spinner, skeleton)
  3. Fetch completes or fails
  4. Set loading to false
  5. 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 getNextPageParam to 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.

Example: Manual Prefetching with Query Client

import { queryClient } from './queryClient';

const prefetchUser = async (userId) => {
  await queryClient.prefetchQuery(
    ['user', userId],
    () => fetchUser(userId),
    {
      staleTime: 5 * 60 * 1000,
      cacheTime: 10 * 60 * 1000,
    }
  );
};

Was this article helpful?