React-query: Show loading spinner while fetching data - javascript

Using react-query in new project and having small issue.
Requirement is to show loading spinner on empty page before data is loaded and than after user fetches new results with different query to show spinner above previous results.
First case is pretty easy and well documented:
const { isLoading, error, data } = useQuery(["list", query], () =>
fetch(`https://api/list/${query}`)
);
if (isLoading) return <p>Loading...</p>;
return <div>{data.map((item) => <ListItem key={item.id} item={item}/></div>
But, cant figure out second case - because, after query changes react-query doing fetch of new results and data from useQuery is empty as result I get default empty array and in that case falls to that condition - if (isLoading && !data.length )
const { isLoading, error, data=[] } = useQuery(["list", query], () =>
fetch(`https://api/list/${query}`)
);
if (isLoading && !data.length ) return <p>Loading...</p>;
return <div>
{data.map((item) => <ListItem key={item.id} item={item}/>
{isLoading && <Spinner/>}
</div>

When you have no cache (first query fetch or after 5 min garbage collector), isLoading switch from true to false (and status === "loading").
But when you already have data in cache and re-fetch (or use query in an other component), useQuery should return previous cached data and re-fetch in background.
In that case, isLoading is always false but you have the props "isFetching" that switch from true to false.
In your example, if the variable "query" passed on the array is different between calls, it's normal to have no result. The cache key is build with all variables on the array.
const query = "something"
const { isLoading, error, data } = useQuery(["list",query], () =>
fetch(`https://api/list/${query}`)
);
const query = "somethingElse"
const { isLoading, error, data } = useQuery(["list",query], () =>
fetch(`https://api/list/${query}`)
);
In that case, cache is not shared because "query" is different on every useQuery

You can use is isFetching for every time new data is fetched
const { isFetching, isError, isSuccess, data } = useQuery('key', fnc())
if (isFetching) {
return <LoaderSpinner />
}
else if (isError) {
return "error encountered"
}
return (
{ isSuccess && data?.map((item) => <li>{item?.name}</li>)}
)

The problem with this code is that it is not checking if the data array is empty before attempting to map over it. If the data array is empty, this code will throw an error. To fix this, the code should check if the data array is empty before attempting to map over it.
const { isFetching, isError, isSuccess, data } = useQuery('key', fnc());
if (isFetching) {
return <LoaderSpinner />
} else if (isError) {
return "error encountered"
} else if (isSuccess && data?.length) {
return data?.map((item) => <li>{item?.name}</li>)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Related

How do you catch if an incoming object (from a http request) doesn't have a certain array? (React JS)

I'm fetching an object (with a text value and a few arrays) from an API and transferring those to local variables for use. All is working except for when that object I'm fetching doesn't have one of those arrays and I try to use it the whole site crashes. I'm lost on how to do the error handling here.
import React, { useEffect, useState } from 'react'
import classes from './Streaming.module.css'
const Streaming = (props) => {
const [streamingOn, setStreamingOn] = useState(false)
const [streamingData, setStreamingData] = useState(null)
async function receiveStreaming() {
await fetch(`https://api.themoviedb.org/3/movie/${props.movie}/watch/providers?
api_key=35135143f12a5c114d5d09d17dfcea12`)
.then(res => res.json())
.then(result => {
setStreamingData(result.results.US)
setStreamingOn(true)
}, (error) => {
console.error("Error: ", error)
}
)
// console.log(data)
}
const displayStreaming = streamingData => {
let sortedData = { ...streamingData }
let streamData = sortedData.flatrate
let rentData = sortedData.rent
let linkText = streamingData.link
let id = Math.random()
let streamListItems = streamData.map((movie) =>
<li key={id}>
<a href={linkText}><img className={classes.logoimg} src=. {'https://image.tmdb.org/t/p/w500/' + movie.logo_path}></img></a>
</li>)
let rentListItems = rentData.map((movie) =>
<li key={id}>
<a href={linkText}><img className={classes.logoimg} src={'https://image.tmdb.org/t/p/w500/' + movie.logo_path}></img></a>
</li>)
return (
<React.Fragment>
<p>Stream on</p>
<ul className={classes.logolist}>{streamListItems}</ul>
<p>Rent on</p>
<ul className={classes.logolist}>{rentListItems}</ul>
</React.Fragment>
)
// console.log(sortedData)
}
return (
<React.Fragment>
<button onClick={receiveStreaming}></button>
{<div className={classes.streaminglogos}>
{(streamingOn) && <div>{displayStreaming(streamingData)}</div> }
</div>}
</React.Fragment>
)
}
export default Streaming
Use optional chaining to check the expected array has been received or not.
Assuming that you need to show an error UI when the expected array was not received, then you can set a flag(isErrored) to true and render that conditionally.
Handling Response JSON
if (!result?.results?.US) {
setIsErrored(true);
} else {
setStreamingData(result.results.US)
setStreamingOn(true);
}
Rendering Error UI conditionally
{isErrored && (<ErrorUI />)}
There are a few things you can do here. The first is that you could check if the array exists when you first get it and then append it on to it if it doesn't.
Maybe something like:
if(!result.results.US){
result.results.US = []
}
Or you could check if the array exists when you are displaying the data by conditionally rendering the component (or piece of component). If the data does not have the array (using the above method) don't display it.
Hope this helps!

Have a custom hook run an API call only once in useEffect

I have a custom hook in my React application which uses a GET request to fetch some data from the MongoDB Database. In one of my components, I'm reusing the hook twice, each using different functions that make asynchronous API calls.
While I was looking at the database logs, I realized each of my GET requests were being called twice instead of once. As in, each of my hooks were called twice, making the number of API calls to be four instead of two. I'm not sure why that happens; I'm guessing the async calls result in re-renders that aren't concurrent, or there's somewhere in my component which is causing the re-render; not sure.
Here's what shows up on my MongoDB logs when I load a component:
I've tried passing an empty array to limit the amount of time it runs, however that prevents fetching on reload. Is there a way to adjust the custom hook to have the API call run only once for each hook?
Here is the custom hook which I'm using:
const useFetchMongoField = (user, id, fetchFunction) => {
const [hasFetched, setHasFetched] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
if (!user) return;
try {
let result = await fetchFunction(user.email, id);
setData(result);
setHasFetched(true);
} catch (error) {
setError(error.message);
}
};
if (data === null) {
fetchData();
}
}, [user, id, fetchFunction, data]);
return { data, hasFetched, error };
};
This is one of the components where I'm re-using the custom hook twice. In this example, getPercentageRead and getNotes are the functions that are being called twice on MongoDB (two getPercentageRead calls and two getNotes calls), even though I tend to use each of them once.
const Book = ({ location }) => {
const { user } = useAuth0();
const isbn = queryString.parse(location.search).id;
const { data: book, hasFetched: fetchedBook } = useFetchGoogleBook(isbn);
const { data: read, hasFetched: fetchedPercentageRead } = useFetchMongoField(
user,
isbn,
getPercentageRead
);
const { data: notes, hasFetched: fetchedNotes } = useFetchMongoField(
user,
isbn,
getNotes
);
if (isbn === null) {
return <RedirectHome />;
}
return (
<Layout>
<Header header="Book" subheader="In your library" />
{fetchedBook && fetchedPercentageRead && (
<BookContainer
cover={book.cover}
title={book.title}
author={book.author}
date={book.date}
desc={book.desc}
category={book.category}
length={book.length}
avgRating={book.avgRating}
ratings={book.ratings}
language={book.language}
isbn={book.isbn}
username={user.email}
deleteButton={true}
redirectAfterDelete={"/"}
>
<ReadingProgress
percentage={read}
isbn={book.isbn}
user={user.email}
/>
</BookContainer>
)}
{!fetchedBook && (
<Wrapper minHeight="50vh">
<Loading
minHeight="30vh"
src={LoadingIcon}
alt="Loading icon"
className="rotating"
/>
</Wrapper>
)}
<Header header="Notes" subheader="All your notes on this book">
<AddNoteButton
to="/add-note"
state={{
isbn: isbn,
user: user,
}}
>
<AddIcon color="#6b6b6b" />
Add Note
</AddNoteButton>
</Header>
{fetchedNotes && (
<NoteContainer>
{notes.map((note) => {
return (
<NoteBlock
title={note.noteTitle}
date={note.date}
key={note._noteID}
noteID={note._noteID}
bookID={isbn}
/>
);
})}
{notes.length === 0 && (
<NoNotesMessage>
You don't have any notes for this book yet.
</NoNotesMessage>
)}
</NoteContainer>
)}
</Layout>
);
};
The way you have written your fetch functionality in your custom hook useFetchMongoField you have no flag to indicate that a request was already issued and you are currently just waiting for the response. So whenever any property in your useEffect dependency array changes, your request will be issued a second time, or a third time, or more. As long as no response came back.
You can just set a bool flag when you start to send a request, and check that flag in your useEffect before sending a request.
It may be the case that user and isbn are not set initially, and when they are set they each will trigger a re-render, and will trigger a re-evalution of your hook and will trigger your useEffect.
I was able to fix this issue.
The problem was I was assuming the user object was remaining the same across renders, but some of its properties did in fact change. I was only interested in checking the email property of this object which doesn't change, so I only passed user?.email to the dependency array which solved the problem.

My Grandparent is not re-rendering the Query with new variables and displaying new data, after the state changes through useState()

So, I am running into this problem where I am making filters while using the one graphql query. There's a child, parent & grandparent(these are different components). Now my query uses variables inside it, initially on load I set the variables in useState and it's working fine. Now when I click on a checkbox(which is insinde Child component) it passed its data(which is variable for new query) to the Grandparent and I am getting that, so I pass that data into the query variable. But it's not re-redering the query again with new variable. So my filters are not working.
Grand Parent
// handling all the product grid actions and data
function ProductGrid() {
const [queryVariables, setQueryVariables] = useState({first: 20});
console.log(queryVariables);
// get the variable object
const { loading, error, data, fetchMore} = useQuery(QUERY, {
variables: queryVariables
});
if (loading) return <p>Loading...</p>;
if (error) return (
<p>
{console.log(error)}
</p>
);
let productEdges = data.products.edges;
return(
<div className="outContainer">
{/* <PriceFilter/> */}
<TypeFilter getFilters={queryVariables => setQueryVariables(queryVariables)} />
{/* test button */}
<div className="product-grid">
{productEdges.map((element, index) => {
// formatting the price
let tempPrice = Math.floor(element.node.priceRange.minVariantPrice.amount);
let productPrice = new Intl.NumberFormat().format(tempPrice);
return(
<div className="container" key={index}>
<div className="image-container">
<img src={element.node.images.edges[0].node.transformedSrc} alt={element.node.title} />
</div>
<div className="product-title">{element.node.title}</div>
<div>{element.node.priceRange.minVariantPrice.currencyCode}. {productPrice}</div>
</div>
)
})}
</div>
{/* load more products button */}
<button
className="load-more"
onClick={()=>{
const endCursor = data.products.edges[data.products.edges.length - 1].cursor;
fetchMore({
variables: {
after: endCursor,
queryVariables
}
})
}}>
Load More
</button>
</div>
)
}
// graphql query for products fetching
const QUERY = gql`
query productFetch($first:Int, $after:String, $query:String){
products(first:$first, after: $after, query:$query){
edges{
node{
priceRange{
minVariantPrice{
amount
currencyCode
}
}
title
images(first:1){
edges{
node{
transformedSrc(maxWidth: 300)
}
}
}
}
cursor
}
pageInfo{
hasNextPage
}
}
}
`
Parent
// ************** Parent ***************
function TypeFilter(props) {
// assume other code is here for a modal pop and
accordion inside here
// passing the prop to checkbox component here and
getting back new state which we use as a callback for
it's parent
<TypeCheckBox getCheckState={queryVariables =>
props.getFilters(queryVariables)} />
}
Child
// ************** Child *****************
let result = "";
let variables =
{
first: 28,
query: ""
};
function TypeCheckBox(props){
// below function returns variables for apollo query
const handleCheckChange = (event) => {
setState({ ...state, [event.target.name]: event.target.checked });
if(event.target.checked){
// pass this value into the productGrid component
if(counter > 1){
result += "&";
}
result = `${result}product_type:${event.target.value}`;
counter++;
// setting filter type to result
console.log(result);
variables.query = result;
console.log(variables);
return props.getCheckState(variables);
}else{
result = result.replace(`product_type:${event.target.value}`, "");
result = removeLast(result, "&");
counter--;
// setting filter type to result
console.log(`in else ${result}`);
variables.query = result;
console.log(variables);
return props.getCheckState(variables);
}
};
}
return (
<FormGroup column>
<FormControlLabel
control={<Checkbox checked={state.checkedBridal} value="Bridal" onChange={handleCheckChange} name="checkedBridal" />}
label="Bridals"
/>
)
}
I tried using useEffect on useQuery in GrandParent, but then the returned constants don't have access outside, like
useEffect(() => {
const { loading, error, data, fetchMore} = useQuery(QUERY, {
variables: queryVariables
});
}, [queryVariables])
Thanks you soo much for your answers ^^
So, I figured out a solution since no one answered it so I am gonna answer it.
first, you need to add refetch and fetch-policy in useQuery hook as
const { loading, error, data, fetchMore, refetch, networkStatus } = useQuery(
QUERY,
{
variables: queryVariables,
// notifyOnNetworkStatusChange: true,
fetchPolicy: "network-only"
}
);
Then you need to make a separate function with the same name as your props that you're passing to your children and use the spread operator with queryVariables to expand and refetch the query with new variables as
const getFilters = queryVariables => {
setQueryVariables({ ...queryVariables });
refetch();
};
Hope this is helpful to someone ^^

React: Abstraction for components. Composition vs Render props

Long question please be ready
I've been working with react-query recently and discovered that a lot of the code has to be duplicated for every component that is using useQuery. For example:
if(query.isLoading) {
return 'Loading..'
}
if(query.isError) {
return 'Error'
}
if(query.isSuccess) {
return 'YOUR ACTUAL COMPONENT'
}
I tried creating a wrapper component to which you pass in the query information and it will handle all the states for you.
A basic implementation goes as follows:
const Wrapper = ({ query, LoadingComponent, ErrorComponent, children }) => {
if (query.isLoading) {
const toRender = LoadingComponent || <DefaultLoader />;
return <div className="grid place-items-center">{toRender}</div>;
}
if (query.isError) {
const toRender = ErrorComponent ? (
<ErrorComponent />
) : (
<div className="dark:text-white">Failed to Load</div>
);
return <div className="grid place-items-center">{toRender}</div>;
}
if (query.isSuccess) {
return React.Children.only(children);
}
return null;
};
And, using it like:
const Main = () => {
const query = useQuery(...);
return (
<Wrapper query={query}>
<div>{query.message}</div>
</Wrapper>
)
}
The issue with this is that query.message can be undefined and therefore throws an error.
Can't read property message of undefined
But, it should be fixable by optional chaining query?.message.
This is where my confusion arises. The UI elements inside wrapper should have been rendered but they donot. And, if they don't then why does it throw an error.
This means that, the chilren of wrapper are executed on each render. But not visible. WHY??
Render Props to rescue
Wrapper
const WrapperWithRP = ({ query, children }) => {
const { isLoading, data, isError, error } = query;
if (isLoading) return 'Loading...'
if (isError) return 'Error: ' + error
return children(data)
}
And using it like,
const Main = () => {
const query = useQuery(...);
return (
<Wrapper query={query}>
{state => {
return (
<div>{state.message}</div>
)
}
}
</Wrapper>
)
}
This works as expected. And the children are only rendered when the state is either not loading or error.
I am still confused as to why the first approach doesn't show the elements on the UI event when they are clearly executed?
Codesandbox link replicating the behaviour: https://codesandbox.io/s/react-composition-and-render-prop-tyyzh?file=/src/App.js
Okay, so i figured it out and it is simpler than it sounds.
In approach one: the JSX will still be executed because it's a function call React.createElement.
But, Wrapper is only returning the children when the query succeeds. Therefore, the children won't be visible on the UI.
Both approaches work, but with approach 1, we might have to deal with undefined data.
I ended up using render-prop as my solution as it seemed cleaner.

React component is rendering old state before getting new data

When the component first renders it gets from the switchcase (enters ALL case because its the default value) on the usePositions hook, the value for positions and I set them there, and return them to the Positions controller.
The problem comes when the selectedClient changes from ALL to TODAY (its a context value, I change it in a Sidebar component somewhere else) and before entering the switch case in TODAY value to get the positions of today, I noticed the Positions component already rendered the old state of ALL positions again! Then a second later it renders correctly the todays positions.
I noticed this because my browser on my network tab shows some calls to the server that IndividualPosition makes it means that it rendered
This is my component where it calls the usePositions hook
export const Positions = () => {
const { selectedClient } = useSelectedClientValue();
const { loading, setLoading } = useLoadingValue();
const { positions } = usePositions(selectedClient);
const clientName =
typeof selectedClient === "string" ? selectedClient : selectedClient.name;
useEffect(() => {
setLoading(false);
}, [positions]);
let positionsView = (
<ul className="positions__list">
{positions.map((position) => (
<IndividualPosition position={position} key={position.positionId} />
))}
</ul>
);
return (
<div className="positions" data-testid="positions">
{!loading ? (
<>
<div className="positions__header">
<h2 data-testid="client-name">{clientName}</h2>
</div>
{positionsView}
</>
) : (
<div className="loading-main-window">
<Spinner />
</div>
)}
</div>
);
};
This is the hook where I fetch the data
export const usePositions = selectedClient => {
const [positions, setPositions] = useState([]);
const {setLoading} = useLoadingValue()
useEffect(() => {
setLoading(true);
switch (selectedClient) {
case 'ALL':
getPositions().then(pos => {
setPositions(pos);
});
break;
case 'TODAY':
getTodayPositions().then(pos => {
setPositions(pos);
});
break;
default:
break;
}
}, [selectedClient]);
return {positions, setPositions};
};
The useEffect runs each time the selectedClient change
It looks like the component renders again before getting the todays data and thats why it shows the old state before getting the new data, but I thought that could be avoided with the loading flag
Basically:
-Positions renders, the hook fetches allPositions, its fine
-If I change in the sidebar the value of the selectedClient context value, the Positions components renders again, rendering the IndividualComponent but with the state of allPositions.
- Instead it should wait till todaysPositions fetches to show the new state (loading should do that)
I already tried having a loading local state (my loading is a context value)
Moving the loading in the useEffects on my local component instead of my hook
Setting loading to false inside the hook after fetching the data
Any ideas?

Categories