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>}
{error && <p>Error: {error}</p>}
{data && (
// Render data
)}
</div>
);
export default withDataFetching('https://api.example.com/data')(HOCExample);
3. Architecting React Apps with React Query
3.1 Introducing React Query:
React Query is a powerful library that simplifies data fetching in React apps. It provides a unified and consistent approach to handling API calls with features like caching, automatic refetching, and more.
3.2 Setting Up React Query:
Start by installing React Query:
npm install react-query
Now, let's configure React Query in our app:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';
const queryClient = new QueryClient();
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.getElementById('root')
);
3.3 Fetching Data with React Query:
Now, let's see how React Query simplifies data fetching in components:
// ReactQueryExample.js
import React from 'react';
import { useQuery } from 'react-query';
const ReactQueryExample = () => {
const { data, isLoading, isError } = useQuery('fetchData', async () => {
const response = await fetch('https://api.example.com/data');
return response.json();
});
return (
<div>
{isLoading && <p>Loading...</p>}
{isError && <p>Error fetching data</p>}
{data && (
// Render data
)}
</div>
);
};
export default ReactQueryExample;
Architecting React Apps with TypeScript and React Query
1. Setting the Foundation: Installing Dependencies
Before we begin, make sure you have React Query and TypeScript installed in your project:
npm install react-query react-query/devtools typescript @types/react-query
2. Configuration in index.tsx
In your index.tsx
, set up React Query by creating a QueryClient
and wrapping your App
component:
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById('root')
);
3. Defining TypeScript Types
Create a types.ts
file to define TypeScript types that will be used throughout the application:
// types.ts
export type ApiResponse<T> = {
data: T;
status: number;
};
export type User = {
id: number;
name: string;
email: string;
};
4. Crafting the Custom Hook for API Calls: useApi.ts
Now, let's create a custom hook, useApi
, that handles API calls using React Query:
// useApi.ts
import { useQuery, UseQueryOptions } from 'react-query';
import { ApiResponse } from './types';
const BASE_URL = 'https://api.example.com';
const fetchApi = async <T>(path: string): Promise<ApiResponse<T>> => {
const response = await fetch(`${BASE_URL}${path}`);
const data = await response.json();
return { data, status: response.status };
};
export const useApi = <T>(path: string, options?: UseQueryOptions<ApiResponse<T>>) => {
return useQuery<ApiResponse<T>, Error>(path, () => fetchApi<T>(path), options);
};
5. Using the Custom Hook in Components
Now, let's put our architecture to use by creating two components, UserList
and PostList
, each using the useApi
hook:
// UserList.tsx
import React from 'react';
import { useApi } from './useApi';
import { User } from './types';
const UserList = () => {
const { data, isLoading, isError } = useApi<User[]>('/users');
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>Error fetching users</p>;
return (
<ul>
{data?.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
// PostList.tsx
import React from 'react';
import { useApi } from './useApi';
import { ApiResponse } from './types';
const PostList = () => {
const { data, isLoading, isError } = useApi<ApiResponse<any>>('/posts');
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>Error fetching posts</p>;
return (
<ul>
{data?.data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
export default PostList;
6. Advantages of this Architecture
Let's briefly discuss the advantages of our architecture:
Reusability: The
useApi
hook promotes code reuse, allowing for consistent and efficient API calls across components.Consistency: Our architecture ensures a uniform approach to handling API calls with React Query, enhancing code predictability.
Type Safety: TypeScript types provide robust type checking, reducing the likelihood of runtime errors and improving code reliability.
Maintainability: The separation of concerns and modular design enhance code maintainability and readability, crucial for long-term project success.
Conclusion:
You've embarked on a journey from basic data fetching with fetch
and Axios to advanced separation of concerns using custom hooks, HOCs, and finally, architecting React apps with React Query. Embrace these techniques, experiment with real-world scenarios, and watch your React applications flourish with maintainability and scalability.
If you think this even gave you an idea about what to expect whether you're starting out in React or even have a few years in your pocket with regards to data-fetching in React, consider liking this article or following for more Frontend tips! Happy coding! 🚀💻
Reach out on Linkedin
Top comments (0)