Hey, fellow React devs!
I am hoping this will be yet another all-encompassing article about React data fetching techniques. This will give you a SOLID understanding of data fetching in React.
Trust me, this is the extent of data fetching. Yes, there are a lot of other use-case-specific data fetching that could happen (like fetching paginated data with proper type-casting for a table component, or sequential promises, etc.), but this is the extent. What I mean is there are an infinite number of add-ons that could be used ON TOP of these.
In this comprehensive guide, we'll start from the basics, gradually progressing to advanced techniques, and finally, we'll architect React apps with a focus on separating concerns using React Query. Whether you're just starting or looking to level up your skills, this blog will equip you with the knowledge to architect React applications like a pro.
1. The Fundamentals: Fetch and Axios in useEffect
1.1 Basics with fetch:
Let's kick off with the fundamental approach using the fetch API in a functional component. We'll explore how to fetch data and update the component's state using the useState and useEffect hooks.
import React, { useState, useEffect } from 'react';
const BasicFetchExample = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []);
return (
<div>
{data ? (
// Render data
) : (
// Render loading state or error message
)}
</div>
);
};
export default BasicFetchExample;
1.2 Axios for Enhanced Functionality:
Building upon the basics, let's explore Axios, a popular HTTP client, to handle data fetching. Axios simplifies error handling and provides additional features like request/response interceptors.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const AxiosExample = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get('https://api.example.com/data');
setData(response.data);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []);
return (
<div>
{data ? (
// Render data
) : (
// Render loading state or error message
)}
</div>
);
};
export default AxiosExample;
2. Advanced Separation of Concerns: Custom Hooks and HOCs
2.1 Custom Hooks for Modularity:
Now, let's introduce the concept of custom hooks. We'll create a reusable data fetching hook, encapsulating the logic for fetching and handling data.
// useDataFetching.js
import { useState, useEffect } from 'react';
const useDataFetching = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
export default useDataFetching;
Now, we can easily use this custom hook in any component:
import React from 'react';
import useDataFetching from './useDataFetching';
const CustomHookExample = () => {
const { data, loading, error } = useDataFetching('https://api.example.com/data');
return (
<div>
{loading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
{data && (
// Render data
)}
</div>
);
};
export default CustomHookExample;
2.2 Higher-Order Components (HOCs) for Reusability:
Another powerful technique for separating concerns is using Higher-Order Components. Let's create a data fetching HOC.
// withDataFetching.js
import React from 'react';
const withDataFetching = (url) => (WrappedComponent) => {
return (props) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return <WrappedComponent data={data} loading={loading} error={error} {...props} />;
};
};
export default withDataFetching;
Now, we can enhance any component with data fetching capabilities:
import React from 'react';
import withDataFetching from './withDataFetching';
const HOCExample = ({ data, loading, error }) => (
<div>
{loading && <p>Loading...</p>}
{