Supercharge Network Performance in React Apps: Harnessing the Power of React Query and Axios
Boosting Network Performance in React Apps with React Query and Axios Network requests play a vital role in the majority of today’s applications. However, often we overlook the importance of optimizing performance. In this article, we will explore an efficient code structure that prioritizes both code readability and network performance.
To make the most of this article, it is recommended to have a working knowledge of ReactJS, JavaScript, and a network request method like Fetch (which we will use as a baseline example)
Axios
Normally we use Fetch API for an API call, and the code would seem like this:
fetch('https://api.example.com/posts', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your_token_here'
}
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
There are several problems using this approach:
Axios
Axios solves our problems here.
axios.get('https://api.example.com/posts', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your_token_here'
}
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
// Add a request interceptor
Axios.interceptors.request.use(
(config) => {
const token = localStorageService.getAccessToken();
if (token) {
config.headers["Authorization"] = "Bearer " + token;
}
return config;
},
(error) => {
Promise.reject(error);
}
);
//Add a response interceptor
Axios.interceptors.response.use(
(response) => {
return response;
},
function (error) {
const originalRequest = error.config;
if (
error.response.status === 401 &&
originalRequest.url === ".../oauth/token"
) {
return Promise.reject(error);
}
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
...API LOGIC
}
return Promise.reject(error);
}
);
We can simply build a function returning a function to build such logic to avoid code repetition.
Recommended by LinkedIn
import axios from 'axios';
// Create a cancel token source
const cancelTokenSource = axios.CancelToken.source();
// Make the request with cancellation token
axios.get('https://api.example.com/data', {
cancelToken: cancelTokenSource.token
})
.then(response => {
// Handle response
console.log(response.data);
})
.catch(error => {
// Handle error (including cancellation error)
if (axios.isCancel(error)) {
console.log('Request canceled:', error.message);
} else {
console.error('Error:', error);
}
});
// Cancel the request
cancelTokenSource.cancel('Request canceled by the user');
API Layer
When I started coding, I used to keep all my API calls in the React JS component wherever I needed them. For example, lets say I am building a Social Media Application, and I needed Posts, I would be calling the API right within the “Posts.jsx” file where I am using the data.
Now imagine in the same example, you have a 100 API calls just like this one and they are all around the application. Then imagine you want to change your backend from NodeJS to Firebase for whatever reason, you would go through all those 100 files to make the corresponding changes.
Hence, to solve this problem, I use what’s called as an API Layer. We create a file in the “src” directory called services.js which includes all the network requests. This way, we can even reuse end points without code duplication.
So now all your API logic is in services.js
import axios from "axios";
import environmentVariables from "./../config/env";
import { FORBIDDEN_RESOURCE_ERROR } from "../config/constants";
const apiUrl = environmentVariables.API_URL;
export const login = async ({ userName, password }) => {
try {
const response = await axios.post(`${apiUrl}/user/login`, {
username: userName,
password: password,
});
localStorage.setItem("accessToken", response.data.accessToken);
return {
error: false,
payload: response,
};
} catch (err) {
return {
error: true,
message: err.message,
payload: err,
responseText: err.response.data.otherMessage,
};
}
};
React Query
Now comes the best part. Normally, in React apps, the logic to perform loading states or error states could be as follows:
const MyComponent = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [data, setData] = useState([])
useEffect(() => {
setLoading(true)
getDataFromApi().then(res => {
// Assuming the response contains an error key
if (res.error) {
setError(true)
setLoading(false)
}
setLoading(false)
setData([...res.data])
}).catch(err => {
setError(true)
setLoading(false)
}
}, [])
return <div>...</div>
}
Notice how this code will be repeated for every single API call.
Instead, look at this much cleaner and shorter version using React Query:
const MyComponent = () => {
const {data, isError, isLoading} = useQuery('data')
// Assuming the response contains an error key
if (data && data.error) {
return <div>An error occurred: {data.response}</div>;
}
return <div>...</div>
}
Caching
Now imagine, your data is not changing at all, and you send network requests again and again only to get the same result. Similarly, in slow networks, you would want your data to remain intact. Which is where caching comes in. The ‘data’ key passed in the useQuery function basically says if the ‘data’ key does not change, then i will not resend an API request. Ofcourse after a period API calls will take place to ensure data isn’t changing on the backend.
Conclusion
The mechanism I provided improves your network performance by a lot. In the next article I will explain how to combine this entire functionality with Next JS to utilize server side logic and combine this to develop a high performant website that can run in slow internet also.
Great article! One thing I feel is missing is how to bring it all together. Either a link to github or just another code dump at the end showing how to use react-query with axios with interceptors.