I’m working with React Query in my React app, and I want to cancel ongoing API requests whenever a new request is triggered. Specifically, I want to ensure that if the user clicks a new button that triggers a request, any previous request should be aborted and not interfere with the new request.
Problem: When a user clicks a button (e.g., "Load New Data"), I want to ensure that if there's already a request in progress, it should be canceled before the new request is made. However, I'm encountering an issue where the ongoing request is not being aborted, and both the ongoing and new requests are executed.
Expected Behavior: If a user triggers a new request, the previous ongoing request should be canceled. Only the new request should be processed.
Code Example: Here is an example of how I tried to handle the situation:
import {
useQuery,
useQueryClient
} from 'react-query';
import axios from 'axios';
const fetchData = async (endpoint) => {
const response = await axios.get(endpoint);
return response.data;
};
export const DataFetchingComponent = () => {
const queryClient = useQueryClient();
const {
data,
isLoading,
isError,
refetch
} = useQuery(
'data',
() => fetchData('/api/data'), {
refetchOnWindowFocus: false,
onSuccess: () => {
console.log('Data loaded successfully');
},
onError: (error) => {
console.log('Error occurred:', error);
},
}
);
const handleButtonClick = (newEndpoint) => {
queryClient.cancelQueries('data'); // Cancel ongoing requests for this query
refetch(newEndpoint); // Trigger the new request
};
return ( <
div >
<
button onClick = {
() => handleButtonClick('/api/newData')
} > Load New Data < /button> {
isLoading ? ( <
p > Loading... < /p>
) : isError ? ( <
p > Error loading data < /p>
) : ( <
p > {
JSON.stringify(data)
} < /p>
)
} <
/div>
);
};
Refactor adminAPI to include a generic endpoint Add a single dynamic endpoint in adminAPI to dynamically select resources. The fetchResource endpoint dynamically builds a query URL based on the resourceType passed in during the call.
The useLazyFetchResourceQuery hook is used to manually trigger queries on button click.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const adminAPI = createApi({
reducerPath: 'dashboardApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com',
}),
endpoints: (builder) => ({
fetchResource: builder.query({
query: ({ resourceType, params }) => {
// Add query parameters dynamically
const queryParams = params
? '?' +
Object.entries(params)
.map(([key, value]) => `${key}=${value}`)
.join('&')
: '';
return `/${resourceType}${queryParams}`;
},
}),
}),
});
export const { useLazyFetchResourceQuery } = adminAPI;
Replace the current multiple hooks with a single dynamic query and include logic to handle unique features based on resourceType and its associated query parameters.
import './App.css';
import React, { useState } from 'react';
import { useLazyFetchResourceQuery } from './service';
function App() {
const [resourceType, setResourceType] = useState(null);
const [queryParams, setQueryParams] = useState(null);
const [fetchResource, { data, isFetching, isError }] = useLazyFetchResourceQuery();
const handleFetchResource = (type) => {
setResourceType(type);
// Customize parameters or behavior for each resource
switch (type) {
case 'posts':
setQueryParams({ _limit: 10, _page: 1 });
break;
case 'comments':
setQueryParams({ _limit: 20 });
break;
case 'albums':
setQueryParams(null);
break;
case 'photos':
setQueryParams({ _limit: 5 });
break;
case 'todos':
setQueryParams(null);
break;
case 'users':
setQueryParams(null);
break;
default:
setQueryParams(null);
}
// Trigger the dynamic query with both resource type and its specific params
fetchResource({ resourceType: type, params: queryParams });
};
return (
<>
<button onClick={() => handleFetchResource('posts')}>Load Posts</button>
<button onClick={() => handleFetchResource('comments')}>Load Comments</button>
<button onClick={() => handleFetchResource('albums')}>Load Albums</button>
<button onClick={() => handleFetchResource('photos')}>Load Photos</button>
<button onClick={() => handleFetchResource('todos')}>Load Todos</button>
<button onClick={() => handleFetchResource('users')}>Load Users</button>
<br />
{/* Show loading state */}
{isFetching && <p>Loading {resourceType}...</p>}
{/* Handle error state */}
{isError && <p style={{ color: 'red' }}>Error loading {resourceType} data.</p>}
{/* Show data dynamically with resource-specific logic */}
{data && (
<div>
<h3>{resourceType.charAt(0).toUpperCase() + resourceType.slice(1)}</h3>
<ul>
{/* Customize how to show data based on the resource */}
{Array.isArray(data) &&
data.map((item, index) => {
switch (resourceType) {
case 'posts':
return <li key={index}><strong>{item.title}</strong> - {item.body}</li>;
case 'comments':
return <li key={index}>"{item.body}" by User {item.email}</li>;
case 'albums':
return <li key={index}>{item.title}</li>;
case 'photos':
return <li key={index}><img src={item.thumbnailUrl} alt={item.title} /> {item.title}</li>;
case 'todos':
return <li key={index}>{item.title} - {item.completed ? '✅' : '❌'}</li>;
case 'users':
return (
<li key={index}>
{item.name} - {item.email}
</li>
);
}
})}
</ul>
</div>
)}
</>
);
}
export default App;
The request is dynamically triggered based on the button clicked. Any ongoing request is automatically canceled and replaced with a new request via built-in AbortController.