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:
-
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. -
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.
-
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.
-
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.
-
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:
-
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.
-
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.
-
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.
-
Error Handling: Dedicated libraries often include built-in error handling features, helping developers manage loading and error states more effectively without manual checks.
-
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.
-
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:
Copy1pnpm create vite my-react-app --template react-ts
Once your project is ready, install TanStack Query by running the following command in your terminal:
Copy1pnpm add @tanstack/react-query
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:
Copy1// 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);
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.
- Define the Post Type: In the
src
folder, create atypes
folder and add aPost.ts
file to define thePost
type.
Copy1// src/types/Post.ts 2export type Post = { 3 userId: number; 4 id: number; 5 title: string; 6 body: string; 7}; 8
- Create API Call Functions: In the
src
folder, create anapi
folder and addgetPosts.ts
andcreatePost.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.
Copy1// 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};
Copy1// 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};
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 yourindex.html
file.
Copy1<!-- Tailwind css --> 2<script src="https://cdn.tailwindcss.com"></script>
- 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.
Copy1// 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;
Copy1// 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;
Now include both in the App.tsx
like this:
Copy1// 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;
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.
- In
Posts.tsx
: We'll use theuseQuery
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.
Copy1// 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;
- In
CreatePost.tsx
: We'll first create twouseState
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 theuseMutation
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.
Copy1// 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;
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.
-
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. -
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:
Copy1pnpm install @tanstack/react-query-devtools
Then, in your main entry point main.tsx
, add the ReactQueryDevtools
component:
Copy1// 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);
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.