JH Logo

React Data Fetching

12min
React Data Fetching

Data Fetching in React

Data fetching is essential in most React apps. Many developers start with useEffect for this, but as apps grow, it leads to issues like re-renders, race conditions, and complex state management. Libraries like SWR, TanStack Query, and RTK Query solve these problems by offering built-in features like caching, automatic updates, and better performance—making data fetching easier and more efficient. In this post, I will provide a code example for using TanStack Query.

Why Not to Use useEffect for Fetching Data

While useEffect is a common choice for data fetching in React, it has several drawbacks that can complicate development, especially as applications grow in complexity:

  1. Lifecycle Pitfalls: useEffect runs after every render, which can lead to unintended behavior. If not carefully managed, it may trigger unnecessary fetches when dependencies change, resulting in performance issues.

  2. Manual State Management: Developers must manually handle loading, success, and error states. This adds boilerplate code and increases the chances of bugs, particularly in complex components.

  3. Race Conditions: When multiple requests are made due to re-renders, race conditions can occur. This may result in displaying outdated data if responses arrive out of order.

  4. Stale Data: There’s no built-in mechanism to refresh data or manage stale states. Developers must implement their own logic for polling or refetching, adding to the complexity.

  5. Code Complexity: As applications scale, the amount of logic needed to manage data fetching with useEffect can lead to bloated components and reduced readability.

For these reasons, many developers find that using dedicated libraries like SWR or TanStack Query leads to cleaner, more maintainable code.

Why Use Dedicated Libraries

When building modern React applications, using dedicated libraries for data fetching, such as SWR, TanStack Query, and RTK Query, offers several advantages over traditional methods like useEffect. Here are a few reasons to consider these dedicated libraries:

  1. Simplified Syntax: Libraries like SWR and TanStack Query provide a more straightforward API for fetching data, reducing boilerplate code and making your components cleaner and easier to read.

  2. Built-in Caching: These libraries often come with built-in caching mechanisms, allowing your application to serve cached data while simultaneously fetching fresh data in the background. This leads to a smoother user experience.

  3. Automatic Revalidation: SWR and TanStack Query typically handle revalidating data automatically when the component mounts or when the network status changes, ensuring users always see the latest information without additional coding effort.

  4. Error Handling: Dedicated libraries often include built-in error handling features, helping developers manage loading and error states more effectively without manual checks.

  5. Advanced Features: Many of these libraries, including TanStack Query, offer advanced capabilities like pagination, query invalidation, and mutations, enabling developers to handle complex data-fetching scenarios with ease.

  6. Integration with Existing Tools: If you are already using state management libraries like Redux Toolkit, consider using RTK Query. It seamlessly integrates with your Redux store, making it easier to manage both local and remote data in a unified manner.

By leveraging dedicated libraries like SWR, TanStack Query, and RTK Query, developers can focus on building features rather than managing the intricacies of data fetching, resulting in cleaner code and a more efficient development process.

TanStack Query Example

In this example, we'll create a simple React application that fetches a list of posts from an API and allows you to create a new post using TanStack Query. We will also demonstrate how to leverage query invalidation to ensure the data displayed is always up to date after creating a new post. Follow the steps below to set it up. You can find the whole example in this GitHub Repo

Step 1: Install TanStack Query

Before you begin, ensure you have a React project created. If you don't have one yet, you can quickly set up a new React project using Vite and with the following pnpm command:

1pnpm create vite my-react-app --template react-ts
Copy

Once your project is ready, install TanStack Query by running the following command in your terminal:

1pnpm add @tanstack/react-query
Copy

Step 2: Set Up the Query Client

In your main.tsx file import the necessary modules then create a new instance of QueryClient, and render your app within the QueryClientProvider like this:

1// src/main.tsx 2import { StrictMode } from "react"; 3import { createRoot } from "react-dom/client"; 4import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5import App from "./App.tsx"; 6 7const queryClient = new QueryClient(); 8 9createRoot(document.getElementById("root")!).render( 10 <StrictMode> 11 <QueryClientProvider client={queryClient}> 12 <App /> 13 </QueryClientProvider> 14 </StrictMode> 15);
Copy

Step 3: Create API Call Functions

In this step, we will define two functions to interact with the JSONPlaceholder API, a free fake online REST API for testing and prototyping. You can check out the full API documentation here. We will also create a Post type to ensure type safety in our TypeScript project.

  1. Define the Post Type: In the src folder, create a types folder and add a Post.ts file to define the Post type.
1// src/types/Post.ts 2export type Post = { 3 userId: number; 4 id: number; 5 title: string; 6 body: string; 7}; 8
Copy
  1. Create API Call Functions: In the src folder, create an api folder and add getPosts.ts and createPost.ts files for fetching posts and creating a new post. We will be using the Fetch API here, but you can use Axios for example.
1// src/api/getPosts.ts 2export const getPosts = async () => { 3 const response = await fetch("https://jsonplaceholder.typicode.com/posts"); 4 if (!response.ok) { 5 throw new Error("Response was not OK"); 6 } 7 return response.json(); 8};
Copy
1// src/api/createPosts.ts 2import { Post } from "../types/Post"; 3 4export const createPost = async (newPost: Omit<Post, "id" | "userId">) => { 5 const response = await fetch("https://jsonplaceholder.typicode.com/posts", { 6 method: "POST", 7 headers: { 8 "Content-Type": "application/json", 9 }, 10 body: JSON.stringify(newPost), 11 }); 12 if (!response.ok) { 13 throw new Error("Response was not OK"); 14 } 15 return response.json(); 16};
Copy

Step 4: Create Components for Displaying and Creating Posts

In this step, we will create two components: Posts.tsx for displaying the list of posts and PostForm.tsx for creating a new post. To style the application, we'll use Tailwind CSS.

You can include Tailwind in your project in two ways:

  • Via CDN: Simply add the Tailwind Play CDN link in the <head> of your index.html file.
1<!-- Tailwind css --> 2<script src="https://cdn.tailwindcss.com"></script>
Copy
  • Install Tailwind: For full configuration, you can follow the installation guide here.

Create a Components Folder: In the src folder, create a components folder and add the following two files:

  • Posts.tsx: This component will display the list of posts.
  • PostForm.tsx: This component will allow users to create a new post.
1// src/components/Posts.tsx 2import React from "react"; 3const Posts = () => { 4 5 return ( 6 <> 7 <div className="mt-6 grid grid-cols-2 gap-4"> 8 Posts will come here 9 </div> 10 </> 11 ); 12}; 13 14export default Posts;
Copy
1// src/components/PostForm.tsx 2import React from "react"; 3 4const PostForm = () => { 5 return ( 6 <> 7 <form className="w-1/3 mx-auto mt-6 flex flex-col gap-4"> 8 <div className="flex flex-col"> 9 <label className="font-semibold" htmlFor="title"> 10 Title 11 </label> 12 <input 13 className="border-2 rounded p-1 border-black" 14 name="title" 15 type="text" 16 /> 17 </div> 18 <div className="flex flex-col"> 19 <label className="font-semibold" htmlFor="content"> 20 Content 21 </label> 22 <textarea 23 rows={3} 24 className="border-2 rounded border-black p-1" 25 name="content" 26 ></textarea> 27 </div> 28 29 <button 30 className="bg-black text-white rounded w-fit mx-auto py-2 px-4" 31 type="submit" 32 > 33 Create Post 34 </button> 35 </form> 36 </> 37 ); 38}; 39 40export default PostForm;
Copy

Now include both in the App.tsx like this:

1// src/App.tsx 2import Posts from "./components/Posts"; 3import PostForm from "./components/PostForm"; 4 5function App() { 6 return ( 7 <div className="h-screen flex"> 8 <div className="w-1/2 overflow-auto p-4"> 9 <h1 className="text-center font-bold text-3xl">Posts</h1> 10 <Posts /> 11 </div> 12 <div className="w-1/2 p-4"> 13 <h1 className="font-bold text-3xl text-center">CreatePost</h1> 14 <PostForm /> 15 </div> 16 </div> 17 ); 18} 19 20export default App;
Copy

Step 5: Use TanStack Query in the Components

In this step, we'll integrate TanStack Query into the Posts.tsx and CreatePost.tsx components to handle data fetching and mutations.

  1. In Posts.tsx: We'll use the useQuery hook from TanStack Query to fetch the posts from the JSONPlaceholder API. This hook will manage loading and error states, cache the data, and revalidate when needed. Once the data is fetched, we'll loop through the received posts and render them in the component.
1// src/components/Posts.tsx 2import React from "react"; 3import { getPosts } from "../api/getPosts"; 4import { useQuery } from "@tanstack/react-query"; 5 6const Posts = () => { 7 const { data, isError, isPending, error } = useQuery({ 8 queryKey: ["posts"], 9 queryFn: getPosts, 10 }); 11 12 if (isPending) { 13 return <div className="mt-4 text-center text-xl">Fetching Posts...</div>; 14 } 15 16 if (isError) { 17 return ( 18 <div className="mt-4 text-center text-xl text-red-600"> 19 Error: {error.message} 20 </div> 21 ); 22 } 23 return ( 24 <> 25 <div className="mt-6 grid grid-cols-2 gap-4"> 26 {data.map((post) => ( 27 <div 28 className="bg-zinc-100 shadow-md rounded-md border p-4" 29 key={post.id} 30 > 31 <h2 className="text-2xl font-semibold">{post.title}</h2> 32 <div className="flex mt-2 text-sm gap-2 text-zinc-500"> 33 <span>Post ID: {post.id}</span> 34 <span>|</span> 35 <span>User ID: {post.userId}</span> 36 </div> 37 <p className="mt-2">{post.body}</p> 38 </div> 39 ))} 40 </div> 41 </> 42 ); 43}; 44 45export default Posts;
Copy
  1. In CreatePost.tsx: We'll first create two useState hooks to get the input data from the form fields (e.g., title and body of the post). After that, we'll create a handler function for the form submission where we will use the useMutation hook to make a POST request. After a successful post, we will invalidate the existing post queries to ensure the list of posts gets updated with the newly created post.
1// src/components/PostForm.tsx 2import React, { useState } from "react"; 3import { useMutation, useQueryClient } from "@tanstack/react-query"; 4import { createPost } from "../api/createPost"; 5 6const PostForm = () => { 7 const [title, setTitle] = useState(""); 8 const [body, setBody] = useState(""); 9 10 const queryClient = useQueryClient(); 11 12 const mutation = useMutation({ 13 mutationFn: createPost, 14 onSuccess: () => { 15 queryClient.invalidateQueries({ queryKey: ["posts"] }); 16 }, 17 }); 18 19 const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => { 20 e.preventDefault(); 21 mutation.mutate({ title, body }); 22 }; 23 24 if (mutation.isPending) { 25 return <div className="mt-4 text-center text-xl">Creating Post...</div>; 26 } 27 28 if (mutation.isSuccess) { 29 return ( 30 <div className="mt-4 text-center text-xl text-green-600"> 31 Post Created! 32 </div> 33 ); 34 } 35 36 if (mutation.isError) { 37 return ( 38 <div className="text-center text-xl text-red-600"> 39 Error while creating the post 40 </div> 41 ); 42 } 43 44 return ( 45 <> 46 <form 47 onSubmit={(e) => handleSubmit(e)} 48 className="w-1/3 mx-auto mt-6 flex flex-col gap-4" 49 > 50 <div className="flex flex-col"> 51 <label className="font-semibold" htmlFor="title"> 52 Title 53 </label> 54 <input 55 onChange={(e) => setTitle(e.target.value)} 56 value={title} 57 className="border-2 rounded p-1 border-black" 58 id="title" 59 type="text" 60 /> 61 </div> 62 <div className="flex flex-col"> 63 <label className="font-semibold" htmlFor="content"> 64 Content 65 </label> 66 <textarea 67 onChange={(e) => setBody(e.target.value)} 68 value={body} 69 rows={3} 70 className="border-2 rounded border-black p-1" 71 id="content" 72 ></textarea> 73 </div> 74 75 <button 76 className="bg-black text-white rounded w-fit mx-auto py-2 px-4" 77 type="submit" 78 > 79 Create Post 80 </button> 81 </form> 82 </> 83 ); 84}; 85 86export default PostForm;
Copy

Step 6: Inspect the Completed Project

Now that the project is complete, you can inspect how everything works together by testing the app and observing the behavior, especially during network throttling.

  1. Network Throttling: Open your browser’s Developer Tools and throttle the network speed to simulate slower conditions. When you try to create a new post (using the form in CreatePost.tsx), you'll notice that although the JSONPlaceholder API is a mock and won’t actually create a post, the invalidations still work. After the POST request, you'll see an additional API call to fetch all the posts, ensuring the list gets updated properly.

  2. Adding TanStack Query DevTools: To make debugging even easier, you can add TanStack Query DevTools to your project. This will allow you to inspect all the queries and mutations happening in the app, see cache states, and get better insight into how TanStack Query manages your data.

To add the DevTools:

1pnpm install @tanstack/react-query-devtools
Copy

Then, in your main entry point main.tsx, add the ReactQueryDevtools component:

1// src/main.tsx 2import { StrictMode } from "react"; 3import { createRoot } from "react-dom/client"; 4import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5import App from "./App.tsx"; 6import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 7import "./index.css"; 8 9const queryClient = new QueryClient(); 10 11createRoot(document.getElementById("root")!).render( 12 <StrictMode> 13 <QueryClientProvider client={queryClient}> 14 <App /> 15 <ReactQueryDevtools initialIsOpen={false} /> 16 </QueryClientProvider> 17 </StrictMode> 18);
Copy

Once added, you’ll see a button at the bottom of your app to open the DevTools panel. Here, you can observe all the queries and mutations, their states, and the corresponding network requests happening in your app.

With these tools and steps, you can now explore how TanStack Query manages your API calls, caching, and invalidation processes. Even though the JSONPlaceholder API is a mock and doesn't save data, you can still see how query invalidation works efficiently to update the list of posts.

Back to Blog