I have a function which fetches data.
const fetchData = async (filter) => {
if (loading) return
loading = true
const data = await api(filter)
setData(data)
loading = false
}
I also have a filter component, when I change filters it calls my fetchData()
function with the new filter variable.
This all works, however there is an issue
This issue occurs when I switch my filter but my fetching function is in a loading state. This causes the if check to fail and I now see outdated data because a re-fetch never happens.
My initial idea was to create a const q = []
variable, and inside if (loading)
i would push my filters, and somehow at the end I would re-fetch with the last element inside my q
array and then clear that array.
I dont really know how to do that re-fetch logic. A setInterval(() => checkQ(), 1000)
? Doesn't seem right
What would be a better approach to take?
CodePudding user response:
You should use an AbortController
- that's part of the fetch
, as my experience tells me that it's not hard to initiate a new fetch
request, but what to do with the first request, when you send out a second?
Here's a snippet that will do the thing you asked - but also deals with the unnecessary requests:
const { useState, useEffect } = React
const useFetchData = () => {
const [users, setUsers] = useState([])
let controller = null
const fetchData = () => {
console.log('fetch initiated')
if (controller) controller.abort()
controller = new AbortController();
const { signal } = controller;
fetch('https://jsonplaceholder.typicode.com/users', { signal })
.then(response => {
console.log('request response')
return response.json()
})
.then(json => {
console.log('retrieved list:', json)
setUsers(() => json || [])
})
.catch(err => {
if(err.name === "AbortError") {
console.warn('Abort error', err)
}
})
}
return { fetchData }
}
const FetchData = () => {
const { fetchData } = useFetchData()
return (
<div>
<button onClick={fetchData}>FETCH DATA</button><br />
</div>
)
}
const FetchAbortFetchData = () => {
const { fetchData } = useFetchData()
return (
<div>
<button onClick={() => {
fetchData()
fetchData()
}}>FETCH-ABORT-FETCH DATA</button><br />
</div>
)
}
const App = () => {
return (
<div>
<FetchData /><br />
<FetchAbortFetchData />
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
CodePudding user response:
Easiest way is to use your filter criteria as the lock.
Advantages
- Always fetching data immediately
- Only calling setData with the results from the most recent filter criteria
- Simple
Disadvantages
- May have multiple simultaneous requests
- Toggling back/forth between filters may lead to a race condition.
let latestFilter = null;
const fetchData = async (filter) => {
// Check to see if its there's the same request already in progress
// Note: may need to deep equal to check the filter
if (filter === latestFilter) return;
// Set the current request as most up to date
latestFilter = filter
// Fetch Data (async)
const data = await api(filter)
// If the filter is still the most up to date, use it. Otherwise discard.
// Note: may need to deep equal to check the filter
if (filter === latestFilter) {
setData(data)
latestFilter = null
}
}
In order to solve disadvantage 2, you can include a counter. This will ensure that only the most recent request will run setData.
let latestFilter = null;
let latestRequestNumber = 0
const fetchData = async (filter) => {
if (filter === latestFilter) return;
latestFilter = filter
// Identify the current request
const requestNumber = latestRequestNumber 1;
// Store this number
latestRequestNumber = requestNumber
const data = await api(filter)
// Update data only if we are the most recent request.
if (callCount = latestCallCount) {
setData(data)
latestFilter = null
}
}