Quick Summary
This blog covers essential best practices for React data fetching, including using useEffect for side effects, leveraging React Query for caching and background updates, and optimizing performance with throttling and pagination. It also highlights the importance of avoiding redundant API calls through memoization and proper use of hooks like useMemo and useCallback. By following these techniques, developers can improve the efficiency and reliability of their React applications while ensuring smooth user experiences.
Introduction:
Fetching data efficiently is a crucial aspect of building modern React applications. Whether you’re working with small datasets or large-scale applications, optimizing data fetching can significantly enhance performance and user experience. In this blog, we’ll walk through some of the best practices for fetching data in React, offering tips and techniques that will help you handle asynchronous operations like a pro. From using the right hooks to caching strategies, let’s dive into the world of efficient data fetching in React.
If you’re looking to implement these best practices seamlessly into your project, consider partnering with a Reliable React Development Company. Our team of experts can help you build fast, scalable, and efficient React applications tailored to your needs.
1) Use useEffect for Data Fetching:
Leverage the `useEffect` hook to perform data fetching in functional components. This hook allows you to handle side effects in a declarative way, ensuring that data fetching occurs after the component has rendered.
Example:
import { useEffect, useState } from 'react';
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();}, []); // Empty dependency array ensures useEffect runs once.
Don’t Recommended useEffect for API Calls:
https://react.dev/reference/react/useEffect#what-are-good-alternatives-to-data-fetching-in-effects
2) Cache Data with useMemo or React Query:
To optimize performance and reduce redundant API calls, consider caching the fetched data. You can use `useMemo` to memoize the data or explore libraries like React Query, which provides powerful caching, background updates, and other features.
React Query is a light caching layer that lives in our application. As a data-fetching library, it is agnostic to how we fetch our data. The only thing React Query needs to know is the promise returned by Axios or Fetch.
The two main concepts of React Query are queries and mutation. While queries deal with fetching data, mutations handle modifying data on the server.
React Query exports a useQuery hook for handling queries. The useQuery hook takes two parameters. The first parameter is a unique identifier to describe what we are fetching. And the second identifier is the fetcher function — an async function responsible for either getting your data or throwing an error.
Example:
1) Creating API Call Hook:
import { useQuery } from 'react-query';
import axios from 'axios';
const fetchPosts = async () => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
return data;
};
const usePosts = () => useQuery('posts', fetchPosts);
export default usePosts;
2) Using that hook in Screen:
import React from 'react';
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
import usePosts from '../hooks/usePosts';
import { Text } from '../components/Text';
export const Posts = ({ navigation }) => {
const { data, isLoading, isSuccess } = usePosts();
return (
<View style={styles.container}>
{isLoading && (
<React.Fragment>
<Text>Loading...</Text>
</React.Fragment>
)}
{isSuccess && (
<React.Fragment>
<Text style={styles.header}>all posts</Text>
<FlatList
data={data}
style={styles.wrapper}
keyExtractor={(item) => `${item.id}`}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() =>
navigation.push('Post', { post: item })}
style={styles.post}
>
<View style={styles.item}>
<Text style={styles.postTitle}>
{item.title}
</Text>
</View>
</TouchableOpacity>
)}
/>
</React.Fragment>
)}
</View>
);
};
3) Limit Requests with Throttling/Debouncing:
If you’re dealing with user input that triggers multiple API requests (like a search box), it’s a good idea to throttle or debounce requests to avoid overloading your API.
let timeout;
function debounceFetch(query) {
clearTimeout(timeout);
timeout = setTimeout(async () => {
const response = await fetch(
`https://api.example.com/search?q=${query}`
);
const data = await response.json();
console.log(data);
}, 300); // Wait 300ms after the last keystroke
}
document.querySelector("input").addEventListener("input", (event) => {
debounceFetch(event.target.value);
});
4) Use Pagination for Large Datasets:
When dealing with large datasets, always use pagination to retrieve the data in smaller, more manageable chunks. This reduces load times and ensures better performance for both the server and the client.
async function fetchPaginatedData(page = 1) {
const response = await fetch(`https://api.example.com/data?page=${page}`);
const data = await response.json();
console.log(data);
if (data.hasNextPage) {
fetchPaginatedData(page + 1); // Fetch next page
}
}
fetchPaginatedData();
5) Promises:
In React, Promises are used to handle asynchronous operations like fetching data from an API. A Promise represents a value that may be available now, or in the future, or never. React handles promises effectively through lifecycle methods or hooks like useEffect. Promises is have three stage to work on
- Pending: This is representing that the asynchronous operation is ongoing, and the result is not available yet.
- Resolved (Fulfilled): When the operation is successful, a state is changed to the resolved state. It contains the result data of the operation.
- Rejected: If there is an error during the operation, a state is changed to the rejected state. It contains an error object with details about the failure.
import React, { useEffect, useState } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
callApi();
}, []);
const callApi = () => {
fetchData()
.then((result) => {
setData(result);
})
.catch((error) => {
setError(error.message);
});
}
// Using a Promise to simulate an asynchronous operation
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Add an error condition to reject with new Error
// reject(new Error("Failed to fetch data"));
resolve('Data fetched!');
}, 1000);
});
};
return (
<div>
{error ? (
<div>Error: {error}</div>
) : data ? (
<div>Data: {data}</div>
) : (
<div>Loading...</div>
)}
</div>
);
}
export default MyComponent;
6) Avoid redundant api calls:
>There are two best practices in react to avoid repeated api calls.
1. Memoization:
Memoization is a technique to optimize performance by caching the results of expensive operations and reusing them when the same inputs occur. In React, tools like React.memo, useMemo, and useCallback can prevent unnecessary re-renders or recalculations, which might otherwise trigger redundant API calls.
Let’s consider a scenario where we have a component that fetches data from an API whenever a specific dependency changes:
import React, { useCallback, useState } from "react";
const DataFetcher = React.memo(({ fetchData }) => {
fetchData();
return <div>Data fetched!</div>;
});
const App = () => {
const [dependency, setDependency] = useState(0);
const fetchData = useCallback(() => {
// API call logic
console.log("API called with dependency:", dependency);
}, [dependency]);
return (
<div>
<button onClick={() => setDependency((prev) => prev + 1)}>Update</button>
<DataFetcher fetchData={fetchData} />
</div>
);
};
How It Works:
- React.memo: Prevents re-renders of the DataFetcher component unless its props change.
- useCallback: Ensures that fetchData is re-created only when the dependency changes.
By combining these tools, we ensure that the API call is made only when necessary, avoiding redundant requests caused by re-renders.
2. Avoid API Calls in Every Render
- Another common issue arises when API calls are made on every render of a component. This can be avoided by using useEffect with a proper dependency array.
Example:
import { useEffect, useState } from "react";
const App = () => {
const [data, setData] = useState(null);
useEffect(() => {
// API call only when the component mounts
fetch("/api/data")
.then((response) => response.json())
.then(setData);
}, []); // Empty dependency array
return <div>{data ? data.name : "Loading..."}</div>;
};
Conclusion
By using these best practices, you can significantly improve the efficiency and reliability of data fetching in your React applications. Whether it’s using advanced libraries like React Query, avoiding redundant API calls, or implementing throttling, these techniques ensure optimal performance and a smooth user experience. Start incorporating these tips into your projects today to build faster and more robust React applications! If you need expert assistance in implementing these strategies, consider working with a Trusted React JS Development Company to take your React development to the next level.