I'm trying to show the no results components whenever the api has finished loading and when no results are returned. The issue I'm having, is I am seeing the no results components displayed first for a few seconds and then the results showing whenever the api returns data. I should never want to see the no results component showing if there are results returned from the api.
import React, { useEffect, useState } from 'react';
import NoResults from './NoResults';
const Users = () => {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
const isLoading = () => {
if (isResultsLoading) return <ResultsLoader />;
if (results && results.length > 0)
return (
<UserTableWrapper>
<UserTable
data={results}
/>
</UserTableWrapper>
);
return <NoResults heading="No users available." />;
};
useEffect(() => {
let isMounted = true;
const getData = async () => {
if (isMounted) {
const users = await fetchUsers(); // is just an api call
if (users && users.length > 0) return { users, loading: false };
return { users: null, loading: true };
}
return { users: null, loading: true };
};
getData().then(({ users, loading }) => {
if (isMounted) {
if (users) setResults(users);
setIsResultsLoading(loading);
}
});
return () => {
isMounted = false;
};
}, []);
return (
<>
<h1>Users</h1>
{isLoading()}
</>
);
};
};
export default Users;
Check for the length of results with results.length since results already exists as an empty array.
When you get your data and parse it simply set both the states for results with the data, and isLoading to false.
Perhaps rename the isLoading function to something more representative of what the function does. I've called mine getJSX.
Here's a working example that uses a mock API. (Note I've had to use this without async and await because snippets haven't caught up with the latest Babel version.) You can change the JSON that's returned by the API by uncommenting/commenting out the JSON statements in the first couple of lines.
const {useState, useEffect} = React;
const json= '[1, 2, 3, 4]';
// const json = '[]';
function mockApi() {
return new Promise((res, rej) => {
setTimeout(() => res(json), 3000);
});
}
function Example() {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
function getJSX() {
if (isResultsLoading) return <div>Loading</div>;
if (results.length) {
return results.map(el => <div>{el}</div>);
}
return <div>No users available.</div>;
};
useEffect(() => {
mockApi()
.then(res => JSON.parse(res))
.then(data => {
setResults(data);
setIsResultsLoading(false);
});
}, []);
return <div>{getJSX()}</div>
};
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You should rely on conditional rendering and simplify your logic a little bit.
import React, { useEffect, useState } from "react";
import NoResults from "./NoResults";
const Users = () => {
// This holds the results - default to null till we get a successful API response
const [results, setResults] = useState(null);
// This should be a boolean stating if the API call is pending
const [isResultsLoading, setIsResultsLoading] = useState(true);
useEffect(() => {
const getData = async () => {
const users = await fetchUsers(); // is just an api call
if (users && users.length > 0) {
// if the result is good, store it
setResults(users);
}
// By the way, the api call is finished now
setIsResultsLoading(false);
};
getData();
}, []); // no deps => this effect will run just once, when the component mounts
if (isResultsLoading) {
// Render nothing while API call is pending
return null;
} else {
if (results) {
// The API has returned a good result, so render it!
return (
<UserTableWrapper>
<UserTable data={results} />
</UserTableWrapper>
);
} else {
// No good result, render the fallback component
return <NoResults heading="No users available." />;
}
}
};
export default Users;
You've a lot of extraneous conditionals and code duplication (not as DRY as it could be). Try cutting down on the user checks before you've even updated state, and you likely don't need the mounted check. Conditionally render the UI in the return.
const Users = () => {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
useEffect(() => {
fetchUsers() // is just an api call
.then(users => {
setResults(users);
})
.catch(error => {
// handle any errors, etc...
})
.finally(() => {
setIsResultsLoading(false); // <-- clear loading state regardless of success/failure
});
}, []);
return (
<>
<h1>Users</h1>
{isResultsLoading ? (
<ResultsLoader />
) : results.length ? ( // <-- any non-zero length is truthy
<UserTableWrapper>
<UserTable data={results} />
</UserTableWrapper>
) : (
<NoResults heading="No users available." />
)}
</>
);
};
Related
I am trying to pass a user address into this Fetch Function, set the value of a state variable equal to the address, and then use that address to make an api call. But as expected, everything runs at the same time and the api call fails because it does not receive the user address.
I am relatively new to useEffect, the below is how I assume a function like this should be written, but evidently I am missing something. It does not return any errors, just a undefined value in the log statement I have below.
const Fetch = (props) => {
const api_key = process.env.REACT_APP_API_KEY;
const [addr,setAddr] = useState([])
const [data,setData] = useState([])
useEffect(() => {
async function Get(){
setAddr(props.useraddress)
}
Get();
}, []);
async function GetNFT() {
useEffect(() => {
axios
.get(
`https://flow-testnet.g.alchemy.com/v2/${api_key}/getNFTs/?owner=${addr}&offset=0&limit=10`
)
.then(res=> {
setData(res.data.nfts);
})
.catch(err=> {
console.log(err);
})
},[]);
}
GetNFT();
console.log(data);
return (
<div>
<script>{console.log('Fetch'+addr)}</script>
{/*
<>
{data.map((dat,id)=>{
return <div key={id}>
<FetchData NFTData={dat} />
</div>
})}
</>
*/}
</div>
)
}
You need a single useEffect that would depend on useraddress that you can destructure from the props, and make an api call that uses the useraddress. You don't need to store useraddress in the state.
const api_key = process.env.REACT_APP_API_KEY
const createUrl = addr => `https://flow-testnet.g.alchemy.com/v2/${api_key}/getNFTs/?owner=${addr}&offset=0&limit=10`
const Fetch = ({ useraddress }) => {
const [addr,setAddr] = useState([])
const [data,setData] = useState([])
useEffect(() => {
axios.get(createUrlcreateUrl(useraddress))
.then(res=> {
setData(res.data.nfts)
})
.catch(err=> {
console.log(err)
})
}, [useraddress])
console.log(data)
return (
// jsx
)
}
Note that the useEffect would be triggered on component's mount, and whenever useraddress changes. If useraddress might be empty or undefined when the component mounts, add a condition inside that avoids the call:
useEffect(() => {
if(!useraddress) return // skip the api call if the address is empty/undefined/null
axios.get(createUrlcreateUrl(useraddress))
.then(res => {
setData(res.data.nfts)
})
.catch(err => {
console.log(err)
})
}, [useraddress])
I have a component Browse that used to display search result requested from SearchBar component.
First search query when results state is null
Found some tracks now results state have array of tracks in
I make another search but changing the filter type from By Tracks to By Users (The results should have array of users objects)
I got an error because the render was called before the initResults() function so the results has always array of tracks object inside so I got error of undefined property
The output when I search for the first time :
> init useEffect
> render
The output when I searched another time inside the Browse Component :
> render
> init useEffect
How can I refresh the useEffect when search params change
Any idea ? Thank's !
Browse.js
export default function Browse() {
const [results, setResults] = UseSafeState(null)
const [isLoading, setIsLoading] = UseSafeState(true)
const location = useLocation()
const searchParams = new URLSearchParams(location.search)
const search = searchParams.get('search')
const identifierFilter = searchParams.get('identifier_filter')
const initResults = () => {
setIsLoading(true)
SearchAPI.search(search, identifierFilter)
.then((response) => {
setResults(response.data)
setIsLoading(false)
})
.catch((error) => {
setResults(null)
setIsLoading(false)
})
}
const renderTrackBrowse = () => {
return results.map((result, key) => <div key={key}>{result.track.name}</div>)
}
const renderUserBrowse = () => {
return results.map((result, key) => <div key={key}>{result.user.username}</div>)
}
const renderPage = () => {
console.log('render')
switch (identifierFilter) {
case FiltersTypesSearchBar.TRACKS.name:
return renderTrackBrowse()
case FiltersTypesSearchBar.ARTIST_NAME.name:
return renderUserBrowse()
default:
}
}
useEffect(() => {
console.log('init useEffect')
initResults()
}, [location, search, identifierFilter])
return <div className="main double-contained browse">{isLoading ? <Loader /> : renderPage()}</div>
}
Effects will always execute after the execution of the whole function. Even useLayoutEffect does not get invoked early enough to fix your issue. But with a tiny change, your code would already work.
const renderPage = () => {
console.log('render')
// do not continue execution if no results exist
if (!results) return;
switch (identifierFilter) {
case FiltersTypesSearchBar.TRACKS.name:
return renderTrackBrowse()
case FiltersTypesSearchBar.ARTIST_NAME.name:
return renderUserBrowse()
default:
}
}
Also one of your dependencies in the useEffect seems to be irrelevant and could cause more requests to be done as necessary. This should be sufficient.
useEffect(() => {
console.log('init useEffect')
initResults()
}, [search, identifierFilter])
I have a React component using an infinite scroll to fetch information from an api using a pageToken.
When the user hits the bottom of the page, it should fetch the next bit of information. I thought myself clever for passing the pageToken to a useEffect hook, then updating it in the hook, but this is causing all of the api calls to run up front, thus defeating the use of the infinite scroll.
I think this might be related to React's derived state, but I am at a loss about how to solve this.
here is my component that renders the dogs:
export const Drawer = ({
onClose,
}: DrawerProps) => {
const [currentPageToken, setCurrentPageToken] = useState<
string | undefined | null
>(null);
const {
error,
isLoading,
data: allDogs,
nextPageToken,
} = useDogsList({
pageToken: currentPageToken,
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting) {
setCurrentPageToken(nextPageToken);
}
},
[nextPageToken],
);
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<Drawer
onClose={onClose}
>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
useDogsList essentially looks like this with all the cruft taken out:
import { useEffect, useRef, useState } from 'react';
export const useDogsList = ({
pageToken
}: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
null,
);
const [allDogs, setAllDogs] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result =
await myClient.listDogs(
getDogsRequest,
{
token,
},
);
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken);
// if API returns a pageToken, that means there are more dogs to add to the list
if (nextPageToken) {
setAllDogs((previousDogList) => [
...(previousDogList ?? []),
...newDogs,
]);
}
}
} catch (responseError: unknown) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
};
fetchData();
}, [ pageToken, nextPageToken]);
return {
data: allDogs,
nextPageToken,
error,
isLoading,
};
};
Basically, the api call returns the nextPageToken, which I want to use for the next call when the user hits the intersecting point, but because nextPageToken is in the dependency array for the hook, the hook just keeps running. It retrieves all of the data until it compiles the whole list, without the user scrolling.
I'm wondering if I should be using useCallback or look more into derivedStateFromProps but I can't figure out how to make this a "controlled" component. Does anyone have any guidance here?
I suggest a small refactor of the useDogsList hook to instead return a hasNext flag and fetchNext callback.
export const useDogsList = ({ pageToken }: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
pageToken // <-- initial token value for request
);
const [allDogs, setAllDogs] = useState([]);
// memoize fetchData callback for stable reference
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await myClient.listDogs(getDogsRequest, { token: nextPageToken });
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken ?? null);
setAllDogs((previousDogList) => [...previousDogList, ...newDogs]);
} catch (responseError) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
}, [nextPageToken]);
useEffect(() => {
fetchData();
}, []); // call once on component mount
return {
data: allDogs,
hasNext: !!nextPageToken, // true if there is a next token
error,
isLoading,
fetchNext: fetchData, // callback to fetch next "page" of data
};
};
Usage:
export const Drawer = ({ onClose }: DrawerProps) => {
const { error, isLoading, data: allDogs, hasNext, fetchNext } = useDogsList({
pageToken // <-- pass initial page token
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting && hasNext) {
fetchNext(); // <-- Only fetch next if there is more to fetch
}
},
[hasNext, fetchNext]
);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
// From #stonerose036
// clear previous observer in returned useEffect cleanup function
return observer.disconnect;
}, [handleObserver]);
return (
<Drawer onClose={onClose}>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
Disclaimer
Code hasn't been tested, but IMHO it should be pretty close to what you are after. There may be some minor tweaks necessary to satisfy any TSLinting issues, and getting the correct initial page token to the hook.
While Drew and #debuchet's answers helped me improve the code, the problem around multiple renders ended up being solved by tackling the observer itself. I had to disconnect it afterwards
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
return () => {
observer.disconnect();
};
}, [handleObserver]);
Here is what i tried
episode.js
import Parser from "rss-parser";
import React from "react";
export default function Episode() {
const parser = new Parser();
const url1 = "https://anchor.fm/s/75abc654/podcast/rss";
const [data, setData] = React.useState({});
(async () => {
let data = await parser.parseURL(url1);
setData(data);
// console.log(data.title)
// data.items.forEach((item) => {
// console.log(item.title)
//console.log(item.pubDate.slice(5, 17))
//console.log(item.enclosure.url)
// console.log(item.itunes.image)
});
})();
return(
<h1>{item.title}</h1>
{data.items.map((item, index)=>{
return(
<h1>{item.title}</h1>
)})}
)
}
And the output is blank screen.. No error in console.. Help me to get the data from the rss feed without blank screen
You're calling async method unlimited time! You need to call it once via useEffect just when component rendered for first time:
import Parser from "rss-parser";
import React, { useEffect } from "react";
function Episode() {
const parser = new Parser();
const url1 = "https://anchor.fm/s/75abc654/podcast/rss";
const [data, setData] = React.useState({});
useEffect(() => {
(async () => {
let data = await parser.parseURL(url1);
console.log(data);
setData(data);
})();
}, []);
return (
<>
{data.items?.map((item, index) => (
<h1>{item.title}</h1>
))}
</>
);
}
export default function App() {
return <Episode />;
}
Just replace your async function with a useEffect hook like this
useEffect(() => {
async function fetchMyAPI() {
let data = await parser.parseURL(url1);
setData(data);
// console.log(data.title)
// data.items.forEach((item) => {
// console.log(item.title)
//console.log(item.pubDate.slice(5, 17))
//console.log(item.enclosure.url)
// console.log(item.itunes.image)
}
fetchMyAPI();
}, []);
This will be executed once every time when your component is loaded on screen
Also change the data.item.map to
{data.items?.map((item, index) => {
return <h1>{item.title}</h1>;
})}
Else it will throw error on first render
useEffect(() => {
debugger;
}, [filter]);
// eslint-disable-next-line
useEffect(async () => {
if (parseInt(localStorage.getItem("lastFetchTime")) + 8640000 > Date.now()) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
await fetch('https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2')
.then((resp) => resp.json())
.then((data) => {
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
})
}
}, []);
I have these 2 useEffect in my program, the first one, with the listener is not being called even if the filter is changed. But it works if I remove the [] from the 2nd useEffect and the 2nd one runs on loop so I cant use it like that. I saw multiple forums, all of which suggests this should work.
import { useState, useEffect } from "react";
import { render } from "react-dom";
const sleep = (ms: number) => new Promise(
resolve => setTimeout(() => resolve('Resolved'), ms));
function App() {
const [filter, setFilter] = useState({ count: 0 });
const [get, set] = useState(0);
useEffect(() => {
console.log('Here');
}, [filter]);
useEffect(() => {
async function myFunction() {
const res = await sleep(5000)
.then(res => console.log(res));
setFilter({ ...filter, count: filter.count + 1 });
}
myFunction();
}, [get]);
return (
<div>
<p>App {get}</p>
<button onClick={() => set((get: number) => get + 1)}>
Click
</button>
</div>
);
}
render(<App />, document.getElementById("root"));
This minor snippet to be working for me as expected.
useEffect cannot be async. If you want to call an async function in useEffect() you need to do it like this:
EDIT: this is the complete useEffect
useEffect(() => {
async function getData() {
if (
parseInt(localStorage.getItem("lastFetchTime")) + 8640000 >
Date.now()
) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
const res = await fetch(
"https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2"
);
const data = await res.json();
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
}
}
getData();
}, []);
I tested it and it worked as expected (I console.log() in the other useEffect())
There's nothing wrong with the useEffect. It's a bullet proof. But you make sure the following things:
Is filter updated during the component did mount?
The debugger will show up if you have open developer tool.
Isfilter updated during the component did update?
The debugger won't show up.
To make sure whenfilter is updated, use another effect hook but this time without dependency array.
useEffect(()=>{
console.log(filter) // analyze in the console
})
And if the value is updated during the update then you don't need to use dependency array but check the changes inside the effect hook by using some state for that as filter is coming from the update (props).
import { useState, useEffect, useCallback } from "react";
function App() {
const [isLoading, setIsLoading] = useState(false);
const [filter, setRecipeList] = useState({});
useEffect(() => {
// debugger;
}, [filter]);
// eslint-disable-next-line
const fetchData = useCallback(async () => {
if (
parseInt(localStorage.getItem("lastFetchTime")) + 8640000 >
Date.now()
) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
const data = await fetch(
"https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2"
).then((resp) => resp.json());
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
}
}, []);
useEffect(() => {
setIsLoading(true);
fetchData();
}, [fetchData]);
return (
<div>
<span>{isLoading ? "loading" : "loaded!"}</span>
{!isLoading && filter && <div>filter size:{filter.length}</div>}
</div>
);
}
export default App;
I think it will work properly.
Thanks.