Re-fetching with useQuery on argument change - javascript

I am trying to implement pagination using react-query. On page change I am updating the page inside useEffect using setArrivals. For some reason I am always sending the previous value of the arrivals as the argument for the getProductList function. To fix the issue I am sending the refetch() request inside the setTimeout. It does work but it doesn't feel right to me. Let me know what I am doing wrong.
const HomePage = ({ newArrivals }) => {
const [page, setPage] = useState(1);
const [arrivals, setArrivals] = useState({ ...newArrivals, page: page });
useEffect(() => {
setArrivals((values) => {
console.log({ page });
return { ...values, page: page };
});
setTimeout(function () {
newArrivalsQuery.refetch();
}, 0);
}, [page]);
const newArrivalsQuery = useQuery(
['productListByNewArrivals'],
() => getProductList(arrivals),
{
select: useCallback((data) => {
return JSON.parse(data);
}, []),
}
);
return (
<>
<NewArrivals
newArrivalsQuery={newArrivalsQuery}
page={page}
setPage={setPage}
/>
</>
);
};
export async function getServerSideProps(context) {
const newArrivals = {
sort: 'createdAt',
order: 'desc',
};
try {
const queryClient = new QueryClient();
await queryClient.prefetchQuery('productListByNewArrivals', async () => {
const newArrivalsResult = await listProduct(newArrivals);
return JSON.stringify(newArrivalsResult);
});
return {
props: {
newArrivals: newArrivals,
dehydratedState: dehydrate(queryClient),
},
};
} catch (error) {
console.log('error: ', error);
if (error) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
}
}

The best way is to add the dependencies of your query function to the queryKey. react-query is declarative and will re-fetch automatically if the queryKey changes. If you have to reach to useEffect and refetch, it's likely not the easiest solution:
const HomePage = ({ newArrivals }) => {
const [page, setPage] = useState(1);
const [arrivals, setArrivals] = useState({ ...newArrivals, page: page });
const newArrivalsQuery = useQuery(
['productListByNewArrivals', arrivals],
() => getProductList(arrivals),
{
select: useCallback((data) => {
return JSON.parse(data);
}, []),
}
);
now, arrivals are part of the queryKey, which is what you are using in the queryFn in getProductList. Now all you need to do is call setArrivals and react-query will refetch.
Side note: it looks like arrivals is not really state, but derived state. At least in this snippet, you only call the setter in an effect, which seems wrong. It looks like you want to keep in in-sync with page and compute it every time you call setPage, so you can also do:
const [page, setPage] = useState(1);
const arrivals = { ...newArrivals, page: page };

Related

Search function now working in React photo gallary

Working on a small application that takes a pexels api and displays photos dynamically. When I send the search request for my api to fectch based on the new params, it does actually update the page with new photos but not the ones based on the params. I though I got the search function correct, maybe it's cause I'm not using it in a useEffect? But if I did use it in a useEffect, I wouldn't be able to set it on the onClick handle. I tried to console.log the query I was getting from the onChange but it doesn't seem like it's getting the result. What am I doing wrong?
import { useState, useEffect } from 'react'
import pexelsApi from './components/pexelsApi'
import './App.css'
const App = () => {
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(false);
const [nextPage, setNextPage] = useState(1);
const [perPage, setPerPage] = useState(25);
const [query, setQuery] = useState('');
const [error, setError] = useState('');
useEffect(() => {
const getImages = async () => {
setLoading(true);
await pexelsApi.get(`/v1/curated?page=${nextPage}&per_page=${perPage}`)
.then(res => {
setImages([...images, ...res.data.photos]);
setLoading(false);
}).catch(er => {
if (er.response) {
const error = er.response.status === 404 ? 'Page not found' : 'Something wrong has happened';
setError(error);
setLoading(false);
console.log(error);
}
});
}
getImages();
}, [nextPage, perPage]);
const handleLoadMoreClick = () => setNextPage(nextPage + 1)
const search = async (query) => {
setLoading(true);
await pexelsApi.get(`/v1/search?query=${query}&per_page=${perPage}`)
.then(res => {
setImages([...res.data.photos]);
console.log(res.data)
setLoading(false);
console.log(query)
})
}
if (!images) {
return <div>Loading</div>
}
return (
<>
<div>
<input type='text' onChange={(event) => setQuery(event.target.value)} />
<button onClick={search}>Search</button>
</div>
<div className='image-grid'>
{images.map((image) => <img key={image.id} src={image.src.original} alt={image.alt} />)}
</div>
<div className='load'>
{nextPage && <button onClick={handleLoadMoreClick}>Load More Photos</button>}
</div>
</>
)
};
export default App
import axios from 'axios';
export default axios.create({
baseURL: `https://api.pexels.com`,
headers: {
Authorization: process.env.REACT_APP_API_KEY
}
});
Your main issue is that you've set query as an argument to your search function but never pass anything. You can just remove the arg to have it use the query state instead but you'll then need to handle pagination...
// Helper functions
const getCuratedImages = () =>
pexelsApi.get("/v1/curated", {
params: {
page: nextPage,
per_page: perPage
}
}).then(r => r.data.photos)
const getSearchImages = (page = nextPage) =>
pexelsApi.get("/v1/search", {
params: {
query,
page,
per_page: perPage
}
}).then(r => r.data.photos)
// initial render effect
useEffect(() => {
setLoading(true)
getCuratedImages().then(photos => {
setImages(photos)
setLoading(false)
})
}, [])
// search onClick handler
const search = async () => {
setNextPage(1)
setLoading(true)
setImages(await getSearchImages(1)) // directly load page 1
setLoading(false)
}
// handle pagination parameter changes
useEffect(() => {
// only action for subsequent pages
if (nextPage > 1) {
setLoading(true)
const promise = query
? getSearchImages()
: getCuratedImages()
promise.then(photos => {
setImages([...images, ...photos])
setLoading(false)
})
}
}, [ nextPage ])
The reason I'm passing in page = 1 in the search function is because the setNextPage(1) won't have completed for that first page load.

React Query return undefined data while in network tab the data exists but it does not show on the page

The data is coming from the backend and i can see it in the network tab, but react query is returning an undefined data, it's not even reacting with the data because it returns false loading and fetching in the console, and the devtools is set to fresh(1) . it was working before but now can't seem to figure out a solution .
This is my App.js:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
cacheTime: 0,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient} contextSharing={true}>
<PrivateRoute
exact
path="/dashboard/snp/match"
component={MatchedSequences}
/>
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
);
}
export default App;
This is my component:
const queryClient = useQueryClient();
const [showQuery, setShowQuery] = useState(false);
const [showSequence, setShowSequence] = useState(false);
const { isLoading, isFetching, error, data, status } = useQuery(
"dn",
() => {
fetch("/api/users/find").then((res) => {
return res.json();
});
},
{ keepPreviousData: false },
);
console.log({ isLoading, isFetching });
console.log("error", error);
console.log({ data });
I'm using react Query version 3.34.
Your query function (the second parameter to useQuery) doesn't return anything:
() => {
fetch("/api/users/find").then((res) => {
const result = res.json();
console.log({result});
return result;
});
},
if you use curly brackets for the arrow function, you need the return keyword:
() => {
return fetch("/api/users/find").then((res) => {
const result = res.json();
console.log({result});
return result;
});
},
alternatively, you can use the implicit return from arrow function, but then you have to omit the curly brackets:
() => fetch("/api/users/find").then((res) => {
const result = res.json();
console.log({result});
return result;
});
please update your code to add logging inside of the fetch, like this:
const queryClient = useQueryClient();
const [showQuery, setShowQuery] = useState(false);
const [showSequence, setShowSequence] = useState(false);
const { isLoading, isFetching, error, data, status } = useQuery(
"dn",
() => {
fetch("/api/users/find").then((res) => {
const result = res.json();
console.log({result});
return result;
});
},
{ keepPreviousData: false },
);
console.log({ isLoading, isFetching });
console.log("error", error);
console.log({ data });
What value does result have?

How do I nest async React hooks

This is a massively simplified version a React Hook solution that I am stuck on...
export const useStepA = () => {
const [stepA, setStepA] = useState();
const getStepA = useCallback(async (stepAParam: string) => {
setStepA({ stepBId: '1' });
}, []);
return { getStepA, stepA };
};
export const useStepB = () => {
const [stepB, setStepB] = useState();
const getStepB = useCallback(async (stepBParam: string) => {
setStepB(stepBParam);
}, []);
return { getStepB, stepB };
};
export const useStepC = () => {
const { getStepA, stepA } = useStepA();
const { getStepB, stepB } = useStepB();
const [stepC, setStepC] = useState();
const getStepC = useCallback(
async (stepAParam: string) => {
/* ????? await? useEffect? magic?
getStepA(stepAParam);
getStepB(stepA.stepBId);
*/
setStepC({stepA,stebB});
},
[getStepA, getStepB, stepA]
);
return { getStepC, stepC };
};
In the real world... StepB is dependent on StepA's data, both are fetch calls... StepC takes the contents of StepA and StepB and returns an amalgamation of them...
How I can write the stepC hook to process and wait, then process and wait, then process and return?
While I don't believe this is a very composable pattern to begin with, it is possible to make them work somewhat with a bit of effort if you're careful to make your initial values for stepA and stepB falsy:
export const useStepC = () => {
const { getStepA, stepA } = useStepA();
const { getStepB, stepB } = useStepB();
const [stepC, setStepC] = useState();
const getStepC = useCallback((stepAParam: string) => {
getStepA(stepAParam);
}, [getStepA]);
useEffect(() => {
if (stepA) {
getStepB(stepA.stepBId);
}
}, [stepA, getStepB]);
useEffect(() => {
if (stepA && stepB) {
setStepC({stepA,stebB});
}
}, [stepA, stepB, setStepC])
return { getStepC, stepC };
};
Problems will most likely occur when there are two concurrent promises pending caused by multiple calls to getStepC() in a short period, and because the way useStepA() and useStepB() are implemented, there is no way to resolve these inherent race conditions.
There is a useSWR hook what solve kind of these problems. check this

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]);

useEffect is running when any function is running

First of all, I researched the question a lot, but I could not find a solution. I would appreciate if you help.
functional component
I add the code briefly below. this is not full code
state and props
// blog id
const { id } = props.match.params;
// state
const initialState = {
title: "",
category: "",
admin_id: "",
status: false
};
const [form, setState] = useState(initialState);
const [adminList, setAdminList] = useState([]);
const [articleText, setArticleText] = useState([]);
const [me, setMe] = useState([]);
const [locked, setLocked] = useState(true);
const timerRef = useRef(null);
// queries and mutations
const { loading, error, data } = useQuery(GET_BLOG, {
variables: { id }
});
const { data: data_admin, loading: loading_admin } = useQuery(GET_ADMINS);
const [editBlog, { loading: loadingUpdate }] = useMutation(
UPDATE_BLOG
);
const [lockedBlog] = useMutation(LOCKED_BLOG);
multiple useEffect and functions
useEffect(() => {
if (!loading && data) {
setState({
title: data.blog.title,
category: data.blog.category,
admin_id: data.blog.admin.id,
status: data.blog.status
});
setArticleText({
text: data.blog.text
});
}
console.log(data);
}, [loading, data]);
useEffect(() => {
if (!loading_admin && data_admin) {
const me = data_admin.admins.filter(
x => x.id === props.session.activeAdmin.id
);
setAdminList(data_admin);
setMe(me[0]);
}
}, [data_admin, loading_admin]);
useEffect(() => {
const { id } = props.match.params;
lockedBlog({
variables: {
id,
locked: locked
}
}).then(async ({ data }) => {
console.log(data);
});
return () => {
lockedBlog({
variables: {
id,
locked: false
}
}).then(async ({ data }) => {
console.log(data);
});
};
}, [locked]);
// if loading data
if (loading || loading_admin)
return (
<div>
<CircularProgress className="loadingbutton" />
</div>
);
if (error) return <div>Error.</div>;
// update onChange form
const updateField = e => {
setState({
...form,
[e.target.name]: e.target.value
});
};
// editor update
const onChangeEditor = text => {
const currentText = articleText.text;
const newText = JSON.stringify(text);
if (currentText !== newText) {
// Content has changed
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setArticleText({ text: newText });
if (!formValidate()) {
timerRef.current = setTimeout(() => {
onSubmitAuto();
}, 10000);
}
}
};
// auto save
const onSubmitAuto = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
editBlog({
variables: {
id,
admin_id,
title,
text: articleText.text,
category,
status
}
}).then(async ({ data }) => {
console.log(data);
});
};
// validate
const formValidate = () => {
const { title, category } = form;
return !title || !articleText.text || !category;
};
// clear state
const resetState = () => {
setState({ ...initialState });
};
return (
// jsx
)
first issue, when call onSubmitAuto, first useEffect is running again. i dont want this.
because I just want it to work on the first mount.
second issue, if the articleText state has changed before, when mutation it does not mutate the data in the form state. but if the form state changes first, it mutated all the data. I think this issue is the same as the first issue.
I hope I could explain the problem. :/
Ciao, I have an answer to the first issue: when onSubmitAuto is triggered, it calls editBlog that changes loading. And loading is on first useEffect deps list.
If you don't want that, a fix could be something like that:
const isInitialMount = useRef(true);
//first useEffect
useEffect(() => {
if(isInitialMount.current) {
if (!loading && data) {
setState({
title: data.blog.title,
category: data.blog.category,
admin_id: data.blog.admin.id,
status: data.blog.status
});
setArticleText({
text: data.blog.text
});
}
console.log(data);
if (data !== undefined) isInitialMount.current = false;
}
}, [loading, data]);

Categories

Resources