react-query useQuery skipping entries when fetching asynchronously - javascript

In a high level overview I am building a tinder like app functionality. Where in my example, 4 entries are fetched from my DB, the user is shown one at a time and can click a like or a dislike button. Each button click triggers some asynchronous functions for writing the event to my DB. Once the last entry that was fetched is clicked I need to go and fetch the next 4 entries in my DB.
MY COMPONENT
export const HomePage: React.FC = () => {
const { user } = useUser()
const userId = user?.id
const [current, setCurrent] = useState<number | null>(0)
const [skip, setSkip] = useState(0)
const [url, setUrl] = useState(
`/api/swipes/get-users/?userId=${user?.id}&skip=${skip}`
)
const getUsers = async (url: string) => {
const { data } = await axios.get(url)
return data
}
const { error, data, isLoading, refetch } = useQuery(
['userSwipeData', url],
() => getUsers(url),
{
enabled: !!user?.id,
}
)
const handleRefetchUsers = () => {
setCurrent(0)
setUrl(
`/api/swipes/get-users/?userId=${user?.id}&skip=${
skip + FETCH_USERS_PAGINATION_LIMIT
}`
)
refetch()
setSkip(skip + FETCH_USERS_PAGINATION_LIMIT)
}
const handleSwipe = async (e: any) => {
if (current === null || !data) return
const { value } = e.target
await handleSwipeType(value, data?.users[current].id)
}
const handleSwipeType = async (type: string, id: string) => {
const values = {
userSwipeOn: id,
currentUser: userId,
}
// if (type === 'YES') {
// if (current && data?.users[current]?.isMatch) {
// await axios.post('/api/swipes/create-match', values)
// alert('You have a match!')
// }
// await axios.post('/api/swipes/like', values)
// } else {
// await axios.post('/api/swipes/dislike', values)
// }
if (current === data.users.length - 1) {
handleRefetchUsers()
} else {
setCurrent(current! + 1)
}
}
if (isLoading || current === null) return <Box>Fetching users...</Box>
if (error) return <Box>An error has occurred </Box>
if (data && !data?.users.length) return <Box>No more users for now</Box>
return (
<Box>
<h1>Current User: {data?.users[current].email}</h1>
<Flex>
<button value="NO" onClick={handleSwipe}>
NO
</button>
<button value="YES" onClick={handleSwipe}>
YES
</button>
</Flex>
</Box>
)
}
The problem I am facing is that in this current state when the handleRefetchUsers() function triggers it works as expected. However, if I am to uncomment all the asynchronous code which I need to run on every click to document the event, once the handleRefetchUsers() trigger I notice it is skipping 4 entries every time it runs. I'm really at a loss as to why because the check for a final entry should only run after the async code has finished. Any ideas would be helpful.

I'm pretty sure that refetch doesn't wait for setUrl to actually update the url
You shouldn't really base one state on another state
To fix it I would replace
const [url, setUrl] = useState(
`/api/swipes/get-users/?userId=${user?.id}&skip=${skip}`
)
with
const url = /api/swipes/get-users/?userId=${user?.id}&skip=${skip}`
and remove refetch entirely. react-query will refetch anyway because the url changed

There are a few things that could be improved, but your main issue is that setState is async. So when you use setUrl and then call refetch, refetch is still looking at the old url value.
I think a cleaner way would be to use refetch inside an effect, that has current and skip in the dependency array.
Also, url is a derived state, so it doesn't really need its own state. And you should also be using an arrow function when a new state relies on the previous state - again because setState is async, and it's possible that you are referencing an old state.
const buildUrl = (user, skip) => user?.id ? `/api/swipes/get-users/?userId=${user?.id}&skip=${skip}` : ''
export const HomePage: React.FC = () => {
const { user } = useUser()
const userId = user?.id
const [current, setCurrent] = useState<number>(0) // current page
const [skip, setSkip] = useState(0)
const getUsers = async (url: string) => {
const { data } = await axios.get(url)
return data
}
const { error, data, isLoading, refetch } = useQuery(
['userSwipeData', buildUrl(user, skip)],
() => getUsers(buildUrl(user, skip)),
{
enabled: !!user?.id,
}
)
const handleRefetchUsers = () => {
setCurrent(0)
setSkip((prev) => prev + FETCH_USERS_PAGINATION_LIMIT)
}
const handleSwipe = async (e: any) => {
if (current === null || !data) return
const { value } = e.target
await handleSwipeType(value, data?.users[current].id)
}
const handleSwipeType = async (type: string, id: string) => {
const values = {
userSwipeOn: id,
currentUser: userId,
}
if (type === 'YES') {
if (current && data?.users[current]?.isMatch) {
await axios.post('/api/swipes/create-match', values)
alert('You have a match!')
}
await axios.post('/api/swipes/like', values)
} else {
await axios.post('/api/swipes/dislike', values)
}
if (current === data.users.length - 1) {
handleRefetchUsers()
} else {
setCurrent((prev) => prev + 1)
}
}
useEffect(() => {
refetch()
}, [current, skip])
if (isLoading || current === null) return <Box>Fetching users...</Box>
if (error) return <Box>An error has occurred </Box>
if (data && !data?.users.length) return <Box>No more users for now</Box>
return (
<Box>
<h1>Current User: {data?.users[current].email}</h1>
<Flex>
<button value="NO" onClick={handleSwipe}>
NO
</button>
<button value="YES" onClick={handleSwipe}>
YES
</button>
</Flex>
</Box>
)
}

Related

Empty Object on React useEffect

In my project I have the component ExportSearchResultCSV. Inside this component the nested component CSVLink exports a CSV File.
const ExportSearchResultCSV = ({ ...props }) => {
const { results, filters, parseResults, justify = 'justify-end', fileName = "schede_sicurezza" } = props;
const [newResults, setNewResults] = useState();
const [newFilters, setNewFilters] = useState();
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const [headers, setHeaders] = useState([])
const prepareResults = () => {
let newResults = [];
if (results.length > 1) {
results.map(item => {
newResults.push(parseResults(item));
}); return newResults;
}
}
const createData = () => {
let final = [];
newResults && newResults?.map((result, index) => {
let _item = {};
newFilters.forEach(filter => {
_item[filter.filter] = result[filter.filter];
});
final.push(_item);
});
return final;
}
console.log(createData())
const createHeaders = () => {
let headers = [];
newFilters && newFilters.forEach(item => {
headers.push({ label: item.header, key: item.filter })
});
return headers;
}
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters])
return (
<div className={`flex ${justify} h-10`} title={"Esporta come CSV"}>
{results.length > 0 &&
<CSVLink data={createData()}
headers={headers}
filename={fileName}
separator={";"}
onClick={async () => {
await setNewFilters(filters);
await setNewResults(prepareResults());
await setData(createData());
await setHeaders(createHeaders());
}}>
<RoundButton icon={<FaFileCsv size={23} />} onClick={() => { }} />
</CSVLink>}
</div >
)
}
export default ExportSearchResultCSV;
The problem I am facing is the CSV file which is empty. When I log createData() function the result is initially and empty object and then it gets filled with the data. The CSV is properly exported when I edit this component and the page is refreshed. I tried passing createData() instead of data to the onClick event but it didn't fix the problem. Why is createData() returning an empty object first? What am I missing?
You call console.log(createData()) in your functional component upon the very first render. And I assume, upon the very first render, newFilters is not containing anything yet, because you initialize it like so const [newFilters, setNewFilters] = useState();.
That is why your first result of createData() is an empty object(?). When you execute the onClick(), you also call await setNewFilters(filters); which fills newFilters and createData() can work with something.
You might be missunderstanding useEffect(). Passing something to React.useEffect() like you do
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters]) <-- look here
means that useEffect() is only called, when results or filters change. Thus, it gets no executed upon initial render.

using React useEffect to fetch data and controll component

i am trying to query data using useEffect add those data to state and render them but nothing comes up unless a state in the app changes. this is what i have done so far Please help, Thanks in Advance.
useEffect
// fetchCampaigns
(async () => {
dispatch(showTopLoader());
try {
const res = await getAgentCampaigns(authToken, "accepted");
setCampaigns(res.data.campaigns);
let leads: any[] = [];
const fetchCampaignLeads = async (id: string) => {
try {
const res = await getCampaignLeads(authToken, id);
return res.data.campaignLeads;
} catch (error) {}
};
// loop through campaigns and get leads
let resS: any[] = [];
campaigns.forEach((campaign: any, i: number) => {
const id = campaign?.Campaign?.id;
fetchCampaignLeads(id)
.then((leadsRes) => {
leads.push(leadsRes[i]);
if (id === leadsRes[i]?.campaignId)
return resS.push({
...campaign,
leads: leadsRes,
});
return (resS = campaigns);
})
.catch(() => {})
.finally(() => {
console.log(resS);
setCampaigns(resS);
});
});
} catch (error) {
} finally {
dispatch(hideTopLoader());
}
})();
}, []);
whole component
import { useDispatch, useSelector } from "react-redux";
import _ from "lodash";
import styles from "../../../styles/CreateLeads.module.css";
import {
getAgentCampaigns,
getCampaignLeads,
} from "../../../utils/requests/campaign";
import {
hideTopLoader,
showTopLoader,
} from "../../../store/actions/TopLoader/topLoaderActions";
import CampaignSection from "./CampaignSection";
import Empty from "../Empty/Empty";
import SectionHeader from "../SectionHeader/SectionHeader";
import SearchBar from "../SearchBar/SearchBar";
import { RootState } from "../../../store/store";
const CreateLeadsCardsWrapper: React.FC = () => {
const authToken = useSelector(
(store: any) => store.authenticationReducer.authToken
);
const [stateCampaigns, setStateCampaigns] = React.useState<any[]>([]);
const [showCampaigns, setShowCampaigns] = React.useState<boolean>(false);
const [campaigns, setCampaigns] = React.useState<any[]>(stateCampaigns);
const [filter, setFilter] = React.useState<string>("");
const dispatch = useDispatch();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Reset filter
setFilter("");
let campaignSearch = e.target.value.trim();
if (campaignSearch.length === 0) {
return;
}
let campaignSearchLower = campaignSearch.toLowerCase();
let campaignSearchUpper = campaignSearch.toUpperCase();
let campaignSearchSentence =
campaignSearch.charAt(0).toUpperCase() + campaignSearch.slice(1);
let results = stateCampaigns.filter(
({ leads }: { leads: any[] }, i) =>
leads &&
leads?.some(
(lead: any) =>
lead.firstName.includes(campaignSearch) ||
lead.firstName.includes(campaignSearchLower) ||
lead.firstName.includes(campaignSearchUpper) ||
lead.firstName.includes(campaignSearchSentence) ||
lead.lastName.includes(campaignSearch) ||
lead.lastName.includes(campaignSearchLower) ||
lead.lastName.includes(campaignSearchUpper) ||
lead.lastName.includes(campaignSearchSentence) ||
lead.email.includes(campaignSearch) ||
lead.email.includes(campaignSearchLower) ||
lead.email.includes(campaignSearchUpper) ||
lead.email.includes(campaignSearchSentence) ||
lead.phoneNo.includes(campaignSearch) ||
lead.phoneNo.includes(campaignSearchLower) ||
lead.phoneNo.includes(campaignSearchUpper) ||
lead.phoneNo.includes(campaignSearchSentence)
)
);
setCampaigns(results);
};
React.useEffect(() => {
// fetchCampaigns
(async () => {
dispatch(showTopLoader());
try {
const res = await getAgentCampaigns(authToken, "accepted");
setCampaigns(res.data.campaigns);
let leads: any[] = [];
const fetchCampaignLeads = async (id: string) => {
try {
const res = await getCampaignLeads(authToken, id);
return res.data.campaignLeads;
} catch (error) {}
};
// loop through campaigns and get leads
let resS: any[] = [];
campaigns.forEach((campaign: any, i: number) => {
const id = campaign?.Campaign?.id;
fetchCampaignLeads(id)
.then((leadsRes) => {
leads.push(leadsRes[i]);
if (id === leadsRes[i]?.campaignId)
return resS.push({
...campaign,
leads: leadsRes,
});
return (resS = campaigns);
})
.catch(() => {})
.finally(() => {
console.log(resS);
setCampaigns(resS);
});
});
} catch (error) {
} finally {
dispatch(hideTopLoader());
}
})();
}, []);
React.useEffect(() => {
setCampaigns(stateCampaigns);
campaigns.length > 0 && setShowCampaigns(true);
console.log(showCampaigns);
dispatch(hideTopLoader());
}, []);
return (
<div className={styles.wrappers}>
{/* Multi step form select a campaign first then fill info on the next step */}
<SectionHeader text="Campaign Leads" />
{showCampaigns && stateCampaigns.length === 0 && (
<>
<Empty description="No agents yet" />
<p>Join a campaign.</p>
</>
)}
{showCampaigns && stateCampaigns.length > 0 && (
<>
<p className="text-grey-500">Create Leads for Campaigns.</p>
<section className={styles.container}>
<SearchBar
placeholder="Find Campaign Leads"
onChange={handleChange}
/>
{campaigns.map((item: any) => (
<CampaignSection
key={item?.Campaign?.id}
id={item?.Campaign?.id}
name={item?.Campaign?.name}
imageUrl={item?.Campaign?.iconUrl}
campaignType={item?.Campaign?.type}
productType={item?.Campaign?.Products[0]?.type}
/>
))}
</section>
</>
)}
</div>
);
};
export default CreateLeadsCardsWrapper;
there are two things wrong in yoour code :
1- you should not have two useeffects with the same dependencies in your case: [] you have to merge those useeffects into one or change the second one's dependencies
2- doing async code in useeffect can be problematic sometimes. it is better to create an async function which does the query to the backend and sets the state and the call the function in your useeffect like below :
const getData = async()=>{
// do some queries and set the state
}
React.useeffect(()=>{
getData()
},[])

Updating React state after axios PUT request without having to reload page

On my current application, if a user tries to enter an existing name that has a different number, it will prompt the user if they want to update that entry with the new number. If yes, the entry is updated using an axios PUT request. My issue is that I can only get it to change on the front end by reloading the page (it updates successfully on db.json) instead of it updating immediately after the user confirms. On my useEffect method I tried adding [persons] as the second argument and it seemed to work, but found out that it loops the GET requests infinitely. I have a similar function for when deleting an entry so I'm sure it must be something that has to be added to setPersons
Update methods
const addEntry = (event) => {
event.preventDefault();
const newPersonEntry = {
name: newName,
number: newNumber,
}
const all_names = persons.map(person => person.name.toUpperCase())
const all_numbers = persons.map(person => person.number)
const updatedPerson = persons.find(p => p.name.toUpperCase() === newName.toUpperCase())
const newPerson = { ...updatedPerson, number: newNumber };
if (newName === '') {
alert('Name entry cannot be blank')
return
}
if (newNumber === '') {
alert('Number entry cannot be blank')
return
}
if (all_numbers.includes(newNumber)) {
alert('That number already exists')
return
}
if (newNumber.length < 14) {
alert('Enter a valid number')
return
}
if (all_names.includes(newName.toUpperCase())) {
if (window.confirm(`${newName} already exists, replace number with the new one?`)) {
console.log(`${newName}'s number updated`)
personService
.update(updatedPerson.id, newPerson)
.then(res => {
setPersons() //something here
})
return
}
return
}
personService
.create(newPersonEntry)
.then(person => {
setPersons(persons.concat(person))
setNewName('')
setNewNumber('')
})
}
//PUT exported as personService
const update = (id, newObject) => {
const request = axios.put(`${baseURL}/${id}`,newObject)
return request.then(response => response.data)
}
Other code
const App = () => {
const [persons, setPersons] = useState([])
useEffect(() => {
personService
.getAll()
.then(initialPersons => {
setPersons(initialPersons)
})
}, [])
...
//Display method
const filteredNames = persons.filter(person => person.name.toLowerCase().includes(filter.toLowerCase()))
const row_names = () => {
return (
filteredNames.map(person =>
<p key={person.id}>{person.name} {person.number} <button onClick={() => handleDelete(person)}>delete</button></p>));
}
...
//Render
return (
<div>
<h2>Phonebook</h2>
<h2>Search</h2>
<SearchFilter value={filter} onChange={handleFilterChange} />
<h2>Add Entry</h2>
<Form onSubmit={addEntry}
name={{ value: newName, onChange: handleNameChange }}
number={{ value: newNumber, onChange: handleNumberChange }}
/>
<h2>Numbers</h2>
<DisplayPersons persons={row_names()} />
</div>
)
}
The solution here is a little bit tricky but doable . You need to split your logic into two parts like this :
const [dataChanged , setDataChanged] = useState(false)
useEffect(()=>{
// Rest of your logic here
} , [dataChanged])
useEffect(()=>{
// Your logic will run only one time
// on Success we change the dataChanged state so the other useEffect will
// run basically you can run the rest of your logic in the other
// useEffect so the infinite loop won't happen
// setDataChanged( (prev) => !prev )
} , [])
Was able to use map method that worked
personService
.update(updatedPerson.id, newPerson)
.then(res => {
setPersons(persons.map(p => p.id !== updatedPerson.id ? p : res))
})

Show filtered result from api in React/Typescript

I have a component that fetches data and then I am setting that data via state. If I want to filter that data based on certain criteria or fields from the api, would I do the filter method? or is this not advisable for state objects?
https://stackblitz.com/edit/react-x7tlxr?file=src/App.js
So far I am doing this: but it's not working as the filtering does not happen.
const [fetchData, setFetchData] = React.useState<any>([]);
const [loading, setLoading] = React.useState<boolean>(true);
const [isError, setIsError] = React.useState<boolean>(false);
const url: string = 'https://xxxxxx';
useEffect(() => {
let mounted = true;
const loadData = async (): Promise<any> => {
try {
const response = await axios(url);
if (mounted) {
setFetchData(response.data);
setLoading(false);
setIsError(false);
console.log('data mounted')
}
} catch (err) {
setIsError(true)
setLoading(false);
setFetchData([]);
console.log(err);
}
};
loadData();
return () => {
mounted = false;
console.log('cleaned');
};
},
[url]
);
function to filter based on onClick:
onClick={(idx: number) => {
const resultInnerNew = fetchData.filter((statusPoint: any) => statusPoint.status === 'New');
setFetchData(resultInnerNew)
}
binding to template:
{isError ? <p className="mt-5">There is an error fetching the data!</p> : <div className="container"></div>}
{loading ? <div>Loading ...</div> : <div className="cards row card-container mt-5">
// data here
</div>
}
You could use an arrow function to update the state and filter the data as below,
onClick={(idx: number) => {
setFetchData(data => data.filter(statusPoint => statusPoint.status === 'New')
}}
A quick review on Array.prototype.filter(), this code will only keep the values which have statusPoint.status equal to 'New' in your array.

Cleaning component states useEffect

I have states :
const { id } = useParams<IRouterParams>();
const [posts, setPosts] = useState<IPost[]>([]);
const [perPage, setPerPage] = useState(5);
const [fetchError, setFetchError] = useState("");
const [lastPostDate, setLastPostDate] = useState<string | null>(null);
// is any more posts in database
const [hasMore, setHasMore] = useState(true);
and useEffect :
// getting posts from server with first render
useEffect(() => {
console.log(posts);
fetchPosts();
console.log(hasMore, lastPostDate);
return () => {
setHasMore(true);
setLastPostDate(null);
setPosts([]);
mounted = false;
return;
};
}, [id]);
When component change (by id), I would like to clean/reset all states.
My problem is that all states are still the same, this setState functions in useEffect cleaning function doesn't work.
##UPDATE
// getting posts from server
const fetchPosts = () => {
let url;
if (lastPostDate)
url = `http://localhost:5000/api/posts/getPosts/profile/${id}?limit=${perPage}&date=${lastPostDate}`;
else
url = `http://localhost:5000/api/posts/getPosts/profile/${id}?limit=${perPage}`;
api
.get(url, {
headers: authenticationHeader(),
})
.then((resp) => {
if (mounted) {
if (resp.data.length === 0) {
setFetchError("");
setHasMore(false);
setPosts(resp.data);
return;
}
setPosts((prevState) => [...prevState, ...resp.data]);
if (resp.data.length < perPage) setHasMore(false);
setLastPostDate(resp.data[resp.data.length - 1].created_at);
setFetchError("");
}
})
.catch((err) => setFetchError("Problem z pobraniem postów."));
};
if your component isnt unmounted, then the return function inside useEffect will not be called.
if only the "id" changes, then try doing this instead:
useEffect(() => {
// ... other stuff
setHasMore(true);
setLastPostDate(null);
setPosts([]);
return () => { //...code to run on unmount }
},[id]);
whenever id changes, the codes inside useEffect will run. thus clearing out your states.
OK, I fixed it, don't know if it is the best solution, but works...
useEffect(() => {
setPosts([]);
setHasMore(true);
setLastPostDate(null);
return () => {
mounted = false;
return;
};
}, [id]);
// getting posts from server with first render
useEffect(() => {
console.log(lastPostDate, hasMore);
hasMore && !lastPostDate && fetchPosts();
}, [lastPostDate, hasMore]);

Categories

Resources