Let's go through a counter example using both useEffect and React Query to fetch and display data in a React component, focusing on how each approach handles data fetching and side effects.
We'll assume we're fetching the current count from an API, and this count updates in real-time. The goal is to display the counter and keep it updated with new data from the API.
Scenario 1: Using useEffect
Here, we use useEffect to fetch the counter data and handle the state manually.
import React, { useState, useEffect } from "react";
function Counter() {
const [count, setCount] = useState(null); // State to store the counter
const [loading, setLoading] = useState(true); // State for loading
const [error, setError] = useState(null); // State for error handling
useEffect(() => {
const fetchCounter = async () => {
try {
setLoading(true); // Start loading
const response = await fetch("/api/counter"); // API call to get the counter value
if (!response.ok) {
throw new Error("Error fetching the counter");
}
const data = await response.json();
setCount(data.count); // Set counter value
} catch (err) {
setError(err.message); // Set error state
} finally {
setLoading(false); // Stop loading
}
};
fetchCounter(); // Fetch the counter on mount
}, []); // Empty dependency array means it runs once on mount
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>Counter: {count}</h1>
</div>
);
}
export default Counter;
Explanation:
- State Management: We manually manage three states: count, loading, and error.
- Data Fetching: The fetchCounter function is defined within the useEffect hook, which runs on component mount (empty dependency array).
- Error Handling and Loading: Both loading and error states need to be handled explicitly.
- Re-fetching: If we need to refetch data (e.g., when the user revisits the page or if the window regains focus), we have to implement that logic manually.
Scenario 2: Using React Query
Here, we use React Query to simplify the data fetching process. React Query automatically handles caching, loading, errors, and re-fetching.
import React from "react";
import { useQuery } from "react-query";
function Counter() {
const { data, error, isLoading } = useQuery("counter", async () => {
const response = await fetch("/api/counter");
if (!response.ok) {
throw new Error("Error fetching the counter");
}
return response.json();
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Counter: {data.count}</h1>
</div>
);
}
export default Counter;
Explanation:
- State Management: React Query automatically manages the state (loading, error, data). There's no need to explicitly set up states for loading or error.
- Data Fetching: The useQuery hook simplifies fetching. It automatically manages caching, background updates, and retries.
- Error Handling and Loading: React Query handles these internally, and the hook returns isLoading and error states which can be used directly in the UI.
- Re-fetching: React Query automatically refetches data when the user revisits the page or when the window gains focus. It can also be set up to refetch at intervals or based on custom conditions.
Comparison of the Two Approaches:
Conclusion:
useEffect is great for handling custom or one-time side effects, but when it comes to data fetching, it requires manual state management and more boilerplate code.
React Query simplifies data fetching significantly by abstracting away common tasks such as loading, error handling, and caching. It is ideal for scenarios where you're dealing with frequently changing data or need features like background refetching and caching.