Example to make the context clear:
I am trying to render a component with two sets of data coming from API calls. I am also returning early if the first API call fails. The second API call depends on the data of the first API result. I don't want to combine both effects because that would mean the whole of component does not render till I get bot API results.
This is the psuedo code
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
callApi1();
}, []);
// early return so that all the complex logic below is not called on ever render
if (!dataFromEffect1) return <div>No Data1</div>;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
useEffect(() => {
const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
callApi2();
}, [data1]);
return (
<div>
{/* no need to null check here, because of the early return on top */}
{dataFromEffect1}
{/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
{dataFromEffect2 ? (
<div>
{dataFromEffect2}
</div>
) : null}
</div>
);
};
Problem
The above code does not work because you cannot add a useEffect conditionally (the early return messes it up)
Trying to find the best workaround for this problem.
Just creat a component for dataFromEffect2:
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () =>
setDataFromEffect1(await (await fetch("/api1")).json());
callApi1();
}, []);
if (!dataFromEffect1) return <div>No Data1</div>;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
return (
<div>
{dataFromEffect1}
<Component data1={data1} dataFromEffect2={dataFromEffect2} setDataFromEffect2={setDataFromEffect2} />
</div>
);
};
const Component = ({ data1, dataFromEffect2, setDataFromEffect2 }) => {
useEffect(() => {
const callApi2 = async () =>
setDataFromEffect2(
await (await fetch("/api2", { headers: data1 })).json()
);
callApi2();
}, [data1]);
return <div>{dataFromEffect2}</div>;
};
You can try something like this. It will kill both useEffects, but it will not run the second one unless it retrieved data from the first. Also, I did not fix this in your code but you should not use async code within useEffect. This can lead to memory leaks and unnexpected bugs. You are also not cleaning up the fetch from within the useEffect. Academind has a nice blog explaining how to fix this and what will happen if you keep the code like this https://academind.com/tutorials/useeffect-abort-http-requests/
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
callApi1();
}, []);
useEffect(() => {
if(!dataFromEffect1.length) return;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
callApi2();
}, [dataFromEffect1]);
// early return so that all the complex logic below is not called on ever render
if (!dataFromEffect1) return <div>No Data1</div>;
return (
<div>
{/* no need to null check here, because of the early return on top */}
{dataFromEffect1}
{/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
{dataFromEffect2 ? (
<div>
{dataFromEffect2}
</div>
) : null}
</div>
);
};
Two options ( I went with the second one for now):
create a separate component for abstracting the second useEffect as suggested by #viet in the answer below.
move the useEffect above the early return, but I will have to duplicate the if conditions inside the effect. As described by in the answer below.
Related
I have two Components which are functional. The first one is Parent and the second one is Child and on Parent I have some states and a function which retrieve some data from server and base on it, setData is called then this state is passed to Child to be shown.
The problem is when I first click on a button to trigger that function, state does not change but on second click state is changed and data is shown on Child component.
My Parent is like:
const Parent= () => {
const [data, setData] = useState();
const handleShowDetails = (id) => {
const mainData = data
MyServices.GetData(
command,
(res) => {
mainData = res.name;
}
);
setData(mainData);
}
}
return (
<Child
data={data}
handleShowDetails={handleShowDetails}
/>
);
And my Child is like:
const Child= ({data, handleShowDetails}) => {
return(
<div>
<button onClick={() => handleShowDetails()}/>
{typeof data!== "undefined" ? (
<div className="col-1">
{data}
</div>
) : ("")
</div>
)
}
What can I do to fix this issue?
Note: I implemented MyServices for handling API calls by Axios.
You are setting state even before your GetData call is completed, which must be asynchronous. So setData is called with the old value of data.
Try this:
const handleShowDetails = (id) => {
const mainData = data
MyServices.GetData(
command,
(res) => {
mainData = res.name;
setData(mainData);
}
);
}
Or use await pattern:
const handleShowDetails = async (id) => {
const mainData = data
const ans = await MyServices.GetData(command);
setData(ans.name);
);
}
Note: No need to use mainData i guess.
I am having trouble with a content loading issue with my react/next.js app using hooks. I believe the problem is with my useEffect and the dependency. I would appreciate any unblockin assistance. The situation:
As you will see from the code below:
In the useEffect:
'eventType' object loads from one API endpoint and is added to the corresponding state
if eventType is a movie (has externalData key) then we load the movie object from another API endpoint based on the filmId (in externalData)
so if eventType.externalData is not null then we set the movie state value to the content of the response data
if movie state is true then we render the 'movie' code block
else we render the 'non-movie' code block
The issue:
For some reason the movie information is not coming back fast enough from the API call, so when given the choice of what to render, the app renders the non-movie block
In order to counteract this, I added [movie] to the dependencies for the useEffect. This works, but leads to infinite reload hell.
My question:
How can I make sure that the reload is triggered only once when the movie data loads and not infinitely?
function EventDetailPage(props) {
const router = useRouter();
const slug = router.query.slug;
const cinemaId = router.query.cinemaId;
const date = slug[0];
const eventTypeId = slug[1];
const {locale} = useRouter();
const [eventType, setEventType] = useState(null);
const [movie, setMovie] = useState(null);
const [personData, setPersonData] = useState(null);
const {user} = useUserStore();
const setupEventDetailPageWithData = async () => {
try {
const eventType = await getEventTypeByCinemaIdAndDate(cinemaId, date, eventTypeId);
setEventType(eventType);
console.log('eventType state:', eventType);
if (!user){
const personDataResponse = await fetchPersonData();
setPersonData(personDataResponse);
}
if (eventType.events[0].userId === personData.userId){
console.log("Event initiator id:", eventType.events[0].userId);
setIsInitiator(true);
}
if (eventType.externalData) {
const movie = await getMovieById(eventType.externalData.filmId);
setMovie(movie);
} else {
}
} catch (error) {
// handle error
console.error(error);
}
};
useEffect(() => {
setupEventDetailPageWithData();
}, [movie]);
if (!eventType) {
return (
<>
<LoadingAnimation/>
</>
);
}
if (movie) {
return ( <div> movie code </div> )
} else {
return ( <div> non-movie code </div> )
}
you can do somethings like this
useEffect(() => {
//call api and set your state
},[])
or better practise like this
useEffect(() => {
//call your api and set your state
},[setMovie])
I have a component like this:
const [products, setProducts] = useState([]);
const [store, setStore] = useState([]);
const fetchProducts () => {
... fetch('product_url').then((products) => {
setProducts(products);
}).catch();
}
const fetchStore () => {
... fetch('store_url').then((store) => {
setStore(store);
}).catch();
}
useEffect(() => {
fetchProducts();
fetchStore();
}, []);
const handleLogicAfterProductsAndStoresLoadingSuccess = () => {
// products, store
//Some logic here
}
My question is where can I put the handleLogicAfterProductsAndStoresLoadingSuccess()?
Generally, just test for the values inside your render function, and put any processing after the request.
// Testing for length because you are initializing them to empty arrays. You should probably use null instead.
if (!(products.length && store.length)) {
return <LoadingComponent />
}
// Any other processing you need goes here
return <>
{/* Your result goes here */}
</>;
you can create another useEffect to track both changes of products & store:-
// handle what happen after products & store fetched
const handleLogicAfterProductsAndStoresLoadingSuccess = (products, store) => {
// if both successfully fetched data (not empty array)
if(products.length > 0 && store.length > 0) {
// do something
} else {
alert('Data not fetched correctly!')
}
}
// handle changes happen to products & store (note: this useEffect will be fired every time there are changes made to the products & store)
useEffect(() => {
handleLogicAfterProductsAndStoresLoadingSuccess(products, store)
}, [products, store])
Here is my code:
function StockCard(props) {
const [FetchInterval, setFetchInterval] = useState(300000);
const [StockData, setStockData] = useState({});
const [TrendDirection, setTrendDirection] = useState(0);
const [Trend, setTrend] = useState(0);
const FetchData = async () =>{
const resp = await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`)
setStockData(resp.data);
}
const calculateTrendDirection = () => {
if(StockData.lastPrice.currentPrice > StockData.lastPrice.previousClosePrice){
setTrendDirection(1);
} else if (StockData.lastPrice.currentPrice < StockData.lastPrice.previousClosePrice){
setTrendDirection(-1);
} else {
setTrendDirection(0);
}
}
const calculateTrend = () => {
var result = 100 * Math.abs( ( StockData.lastPrice.previousClosePrice - StockData.lastPrice.currentPrice ) / ( (StockData.lastPrice.previousClosePrice + StockData.lastPrice.currentPrice)/2 ) );
setTrend(result.toFixed(2));
}
useEffect(() => {
FetchData();
const interval = setInterval(async () => {
await FetchData();
}, FetchInterval)
return() => clearInterval(interval);
},[FetchInterval]);
useEffect(()=>{
if(StockData.lastPrice){
console.log("Trends calculated", StockData.name);
calculateTrend();
calculateTrendDirection();
}
},[StockData])
return(
<div>
<CryptoCard
currencyName={StockData.lastPrice? StockData.name : "Name"}
currencyPrice={StockData.lastPrice? `$ ${StockData.lastPrice.currentPrice}` : 0}
icon={<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Bitcoin.svg/2000px-Bitcoin.svg.png"/>}
currencyShortName={StockData.lastPrice? StockData.symbol : "Symbol"}
trend={StockData.lastPrice? `${Trend} %` : 0}
trendDirection={StockData.lastPrice? TrendDirection : 0}
chartData={[9200, 5720, 8100, 6734, 7054, 7832, 6421, 7383, 8697, 8850]}
/>
</div>
)
}
export default StockCard;
The basic idea is. I have a backend from which I fetch data let's say every minute(this is why i need setInterval) and I have cards which are showing off the data i fetched. I have an expression so it says generic things like "Name" until the data has arrived, then it should re-render with the real data.
But this doesn't happen. It fetches all the data, I can log it out but it doesn't get updated.
And error number 2 is it says that in the useEffects i should include the functions into dependencies.
So for example in the second useEffect where I call the function calculateTrend() and calculateTrendDirection, it says I should include not only the StockData but the two functions too.
I tried #Ozgur Sar 's fix and it worked, so it turned out the problem was "timing" with my api calls
I am creating a React.js app which got 2 components - The main one is a container for the 2nd and is responsible for retrieving the information from a web api and then pass it to the child component which is responsible for displaying the info in a list of items. The displaying component is supposed to present a loading spinner while waiting for the data items from the parent component.
The problem is that when the app is loaded, I first get an empty list of items and then all of a sudden all the info is loaded to the list, without the spinner ever showing. I get a filter first in one of the useEffects and based on that info, I am bringing the items themselves.
The parent is doing something like this:
useEffect(() =>
{
async function getNames()
{
setIsLoading(true);
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
useEffect(() =>
{
async function getItems()
{
setIsLoading(true);
const items= await WebAPI.getItems(selectedName);
setAllItems(items);
setIsLoading(false);
};
getTenants();
},[selectedName]);
.
.
.
return (
<DisplayItems items={allItems} isLoading={isLoading} />
);
And the child components is doing something like this:
let spinner = props.isLoading ? <Spinner className="SpinnerStyle" /> : null; //please assume no issues in Spinner component
let items = props.items;
return (
{spinner}
{items}
)
I'm guessing that the problem is that the setEffect is asynchronous which is why the component is first loaded with isLoading false and then the entire action of setEffect is invoked before actually changing the state? Since I do assume here that I first set the isLoading and then there's a rerender and then we continue to the rest of the code on useEffect. I'm not sure how to do it correctly
The problem was with the asynchronicity when using mulitple useEffect. What I did to solve the issue was adding another spinner for the filters values I mentioned, and then the useEffect responsible for retrieving the values set is loading for that specific spinner, while the other for retrieving the items themselves set the isLoading for the main spinner of the items.
instead of doing it like you are I would slightly tweak it:
remove setIsLoading(true); from below
useEffect(() =>
{
async function getNames()
{
setIsLoading(true); //REMOVE THIS LINE
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
and have isLoading set to true in your initial state. that way, it's always going to show loading until you explicitly tell it not to. i.e. when you have got your data
also change the rendering to this:
let items = props.items;
return isLoading ? (
<Spinner className="SpinnerStyle" />
) : <div> {items} </div>
this is full example with loading :
const fakeApi = (name) =>
new Promise((resolve)=> {
setTimeout(() => {
resolve([{ name: "Mike", id: 1 }, { name: "James", id: 2 }].filter(item=>item.name===name));
}, 3000);
})
const getName =()=> new Promise((resolve)=> {
setTimeout(() => {
resolve("Mike");
}, 3000);
})
const Parent = () => {
const [name, setName] = React.useState();
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(false);
const fetchData =(name) =>{
if(!loading) setLoading(true);
fakeApi(name).then(res=>
setData(res)
)
}
const fetchName = ()=>{
setLoading(true);
getName().then(res=> setName(res))
}
React.useEffect(() => {
fetchName();
}, []);
React.useEffect(() => {
if(name)fetchData(name);
}, [name]);
React.useEffect(() => {
if(data && loading) setLoading(false)
}, [data]);
return (
<div>
{loading
? "Loading..."
: data && data.map((d)=>(<Child key={d.id} {...d} />))}
</div>
);
};
const Child = ({ name,id }) =>(<div>{name} {id}</div>)
ReactDOM.render(<Parent/>,document.getElementById("root"))
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>