Infinite Loop from Promise and useState Hook - reactjs

I've managed to accidentally create an infinite loop out of a fetch promise and a useState hook in react. Can anyone point out why this is repeating? My grasp of both pieces is a little weak, and I imagine that a clear explanation with correctly functioning code would help me see how both work.
FYI - the loreReturn variable is an object with some transaction ids, and the getData function goes and grabs some text from the web using those ids.
export function TextExample(){
let testText = "no itemLore yet";
const [text, textAdd] = useState(testText);
const txs = loreReturn.data.transactions.edges;
txs.forEach ((tx) => {arweave.transactions.getData(tx.node.id, {decode: true, string: true}).then(data => {
console.log(data);
textAdd(text + data);
});
});
return (
<div>
<p>{text}</p>
<
/div>
);
};

Put your API calls inside a useEffect hook and add necessary dependencies. This will make sure you are not repeatedly running the API calls every time the component reloads
export function TextExample(){
let testText = "no itemLore yet";
const [text, textAdd] = useState(testText);
useEffect(()=>{
const txs = loreReturn.data.transactions.edges;
txs.forEach ((tx) => {
arweave.transactions.getData(tx.node.id, {decode: true, string: true})
.then(data => {
console.log(data);
textAdd(text + data);
});
});
}, [])
return (
<div>
<p>{text}</p>
</div>
);
};

Related

Why do my ReactJS changes disappear on refreshing the page

I'm new to React and I'm trying to render a list of Pokemons.
I'm fetching the pokemon names from a local file and then using those names to trigger HTTP calls to my backend server, to get the pokemon images. Here's my code so far:
function PokemonList(props) {
const [pokemonList, setPokemonList] = useState([]);
const [isFetched, setIsFetched] = useState(false);
const [renderedList, setRenderedList] = useState([]);
useEffect(() => {
fetch(raw)
.then((r) => r.text())
.then((text) => {
setPokemonList(text.split("\n"));
setIsFetched(true);
});
}, []);
// I believe this is to blame, but I don't know why. On refresh, pokemonList becomes empty
useEffect(() => {
setRenderedList(populateRenderedList(pokemonList));
}, []);
return !isFetched ? null : (
<div className="list">
{renderedList}
<PaginationBar listSize={renderedList.length} list={renderedList} />
</div>
);
}
function populateRenderedList(pokemonList) {
let pokemonAPI = "https://pokeapi.co/api/v2/pokemon-form/";
const temp = [];
console.log(pokemonList);
pokemonList.forEach((pokemonName) => {
let renderedPokemon = (
<a className="pokemonLink" href={pokemonAPI + pokemonName.toLowerCase()}>
<PokemonDiv name={pokemonName.toLowerCase()} />
<h3>{pokemonName}</h3>
</a>
);
temp.push(renderedPokemon);
});
return temp;
}
As I have commented on the code, the 'pokemonList' renders fine when I make any changes to the PokemonList function. But the moment I refresh my page, 'pokemonList' becomes empty. Why is that?
I previously was not using 'useState' to populate my 'renderedList' list. So I believe the problem is happening because I'm using 'useState' , but I don't know why that's happening.
I had tried making 'renderedList' not a state, but I had to, for I am thinking about passing it as props to another child component, in order to change it's state.

React useState overwritten even with spread

So I have a component where I have to make an API call to get some data that has IDs that I use for another async API call. My issue is I can't get the async API call to work correctly with updating the state via spread (...) so that the checks in the render can be made for displaying specific stages related to specific content.
FYI: Project is a Headless Drupal/React.
import WidgetButtonMenu from '../WidgetButtonMenu.jsx';
import { WidgetButtonType } from '../../Types/WidgetButtons.tsx';
import { getAllInitaitives, getInitiativeTaxonomyTerm } from '../../API/Initiatives.jsx';
import { useEffect } from 'react';
import { useState } from 'react';
import { stripHTML } from '../../Utilities/CommonCalls.jsx';
import '../../../CSS/Widgets/WidgetInitiativeOverview.css';
import iconAdd from '../../../Icons/Interaction/icon-add.svg';
function WidgetInitiativeOverview(props) {
const [initiatives, setInitiatives] = useState([]);
const [initiativesStages, setInitiativesStage] = useState([]);
// Get all initiatives and data
useEffect(() => {
const stages = [];
const asyncFn = async (initData) => {
await Promise.all(initData.map((initiative, index) => {
getInitiativeTaxonomyTerm(initiative.field_initiative_stage[0].target_id).then((data) => {
stages.push({
initiativeID: initiative.nid[0].value,
stageName: data.name[0].value
});
});
}));
return stages;
}
// Call data
getAllInitaitives().then((data) => {
setInitiatives(data);
asyncFn(data).then((returnStages) => {
setInitiativesStage(returnStages);
})
});
}, []);
useEffect(() => {
console.log('State of stages: ', initiativesStages);
}, [initiativesStages]);
return (
<>
<div className='widget-initiative-overview-container'>
<WidgetButtonMenu type={ WidgetButtonType.DotsMenu } />
{ initiatives.map((initiative, index) => {
return (
<div className='initiative-container' key={ index }>
<div className='top-bar'>
<div className='initiative-stage'>
{ initiativesStages.map((stage, stageIndex) => {
if (stage.initiativeID === initiative.nid[0].value) {
return stage.stageName;
}
}) }
</div>
<button className='btn-add-contributors'><img src={ iconAdd } alt='Add icon.' /></button>
</div>
<div className='initiative-title'>{ initiative.title[0].value } - NID ({ initiative.nid[0].value })</div>
<div className='initiative-description'>{ stripHTML(initiative.field_initiative_description[0].processed) }</div>
</div>
);
}) }
</div>
</>
);
}
export default WidgetInitiativeOverview;
Here's a link for video visualization: https://vimeo.com/743753924. In the video you can see that on page refresh, there is not data within the state but if I modify the code (like putting in a space) and saving it, data populates for half a second and updates correctly within the component.
I've tried using spread to make sure that the state isn't mutated but I'm still learning the ins and outs of React.
The initiatives state works fine but then again that's just 1 API call and then setting the data. The initiativeStages state can use X amount of API calls depending on the amount of initiatives are returned during the first API call.
I don't think the API calls are necessary for this question but I can give reference to them if needed. Again, I think it's just the issue with updating the state.
the function you pass to initData.map() does not return anything, so your await Promise.all() is waiting for an array of Promise.resolve(undefined) to resolve, which happens basically instantly, certainly long before your requests have finished and you had a chance to call stages.push({ ... });
That's why you setInitiativesStage([]) an empty array.
And what you do with const stages = []; and the stages.push() inside of the .then() is an antipattern, because it produces broken code like yours.
that's how I'd write that effect:
useEffect(() => {
// makes the request for a single initiative and transforms the result.
const getInitiative = initiative => getInitiativeTaxonomyTerm(
initiative.field_initiative_stage[0].target_id
).then(data => ({
initiativeID: initiative.nid[0].value,
stageName: data.name[0].value
}))
// Call data
getAllInitaitives()
.then((initiatives) => {
setInitiatives(initiatives);
Promise.all(initiatives.map(getInitiative))
.then(setInitiativesStage);
});
}, []);
this code still has a flaw (imo.) it first updates setInitiatives, then starts to make the API calls for the initiaives themselves, before also updating setInitiativesStage. So there is a (short) period of time when these two states are out of sync. You might want to delay setInitiatives(initiatives); until the other API requests have finished.
getAllInitaitives()
.then(async (initiatives) => {
const initiativesStages = await Promise.all(initiatives.map(getInitiative));
setInitiatives(initiatives);
setInitiativesStage(initiativesStages)
});

ReactJS context-api won't render after I get data

This is a next.js site, since both my Navbar component and my cart page should have access to my cart's content I created a context for them. If I try to render the page, I get:
Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'key')
obs: The cartContent array exists and has length 1, I can get it by delaying when the data's rendered by using setTimeout, but, can't get it to render right after it's fetched.
I need to make it render after the data from firebase is returned, but always met with the mentioned error.
This is my _app.tsx file
function MyApp({ Component, pageProps }) {
// set user for context
const userContext = startContext();
return (
<UserContext.Provider value = { userContext }>
<Navbar />
<Component {...pageProps} />
<Toaster />
</UserContext.Provider>
);
}
export default MyApp
This file has the startContext function that returns the context so it can be used.
export const startContext = () => {
const [user] = useAuthState(auth);
const [cart, setCart] = useState(null);
const [cartContent, setCartContent] = useState(null);
useEffect(() => {
if (!user) {
setCart(null);
setCartContent(null);
}
else {
getCart(user, setCart, setCartContent);
}
}, [user]);
return { user, cart, setCart, cartContent, setCartContent };
}
This file contains the getCart function.
export const getCart = async (user, setCart, setCartContent) => {
if (user) {
try {
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
if (new_cart) {
let new_cartContent = []
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
console.log(new_cartContent);
setCartContent(new_cartContent);
console.log(new_cartContent);
setCart(new_cart);
}
else {
setCart(null);
setCartContent(null);
}
} catch (err) {
console.error(err);
}
}
}
This is the cart.tsx webpage. When I load it I get the mentioned error.
export default () => {
const { user, cart, cartContent } = useContext(UserContext);
return (
<AuthCheck>
<div className="grid grid-cols-1 gap-4">
{cartContent && cartContent[0].key}
</div>
</AuthCheck>
)
}
I've tried to render the cart's content[0].key in many different ways, but couldn't do it. Always get error as if it were undefined. Doing a setTimeout hack works, but, I really wanted to solve this in a decent manner so it's at least error proof in the sense of not depending on firebase's response time/internet latency.
Edit:
Since it works with setTimeout, it feels like a race condition where if setCartContent is used, it triggers the rerender but setCartContent can't finish before stuff is rendered so it will consider the state cartContent as undefined and won't trigger again later.
Try changing
{cartContent && cartContent[0].key}
to
{cartContent?.length > 0 && cartContent[0].key}
Edit:: The actual problem is in getCart function in line
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
This is either set to an empty array or an empty object. So try changing your if (new_cart) condition to
if (Object.keys(new_cart).length > 0) {
Now you wont get the undefined error
Since there seemed to be a race condition, I figured the setCartContent was executing before its content was fetched. So I changed in the getCart function the map loop with an async function for a for loop
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
to
for (const key of Object.keys(new_cart)) {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
}
I can't make a map function with await in it without making it asynchronous so I the for loop made it work. Hope someone finds some alternatives to solving this, I could only come up with a for loop so the code is synchronous.

In ReactJS, facing problem in rendering the data from an API due to its odd structure

This is my code:
import React, { useState, useEffect } from "react";
import axios from 'axios';
function App() {
const [arrName, setName] = useState();
useEffect(() => {
axios.get('URL')
.then( response => {
// console.log( response.data )
setName( response.data )
});
}, [])
const foo = () => {
console.log(arrName.data[0].data[0]._id)
// following two console statements need not to be in the code. I am putting them only to show the structure of API
console.log(arrName)
console.log(arrName.data)
}
return(
<div>
Hi, I'm the component
<button onClick={foo}> console </button>
</div>
)
}
export default App;
Console response screenshot
I want to simplify the following statement. So that I can easily iterate the API and print anything from the API. However, I am printing only an id here.
console.log(arrName.data[0].data[0]._id)
Following command breaks the code
<div>
arrName.data[0].data.map((item) => <li>{item._id}</li> ) }
</div>
Kindly, Help me what changes should I make to my code.
Based on the replied comment, you would do it like so:
arrName?.data &&
arrName.data.map((item) => (
<ul>
{item.data.map((chidlItem) => (
<li> {chidlItem._id} </li>
))}
</ul>
));
The ? is Optional Chaining which is to check if the reference in the chain is valid, so it will skip the map function if arrName or arrName.data is null/undefined
Can't you just iterate this part?
arrName.data[0].data
for (const d of arrName.data[0].data) {
console.log(d._id)
}
I guess the real problem is from the backend API sending the data structured as an array of objects inside another array of objects. One solution could be saving the value needed in an array that would be more accessible to you.
const [arrName, setName] = useState();
useEffect(() => {
axios.get('URL')
.then( response => {
// console.log( response.data )
setName( response.data[0].data )
});
}, [])
but that will only be a workaround.

map not a function using react hooks

i'm trying to populate a select bar with a name from an API call. I Have created my hook, also useEffect for its side effects, and passed the data down the return. its giving me map is not a function error. my variable is an empty array but the setter of the variable is not assigning the value to my variable. How can i clear the map not a function error ? i have attached my snippet. Thanks.
import React, { useEffect, useState } from "react";
import axios from "axios";
const Sidebar = () => {
const [ingredients, setIngredients] = useState([]);
useEffect(() => {
const fetchIngredients = async (url) => {
try {
let res = await axios.get(url);
setIngredients(res.data);
} catch (error) {
setIngredients([]);
console.log(error);
}
};
fetchIngredients(
"https://www.thecocktaildb.com/api/json/v2/1/search.php?i=vodka"
);
}, []);
const displayIngredients = ingredients.map((ingredient) => {
setIngredients(ingredient.name);
return <option key={ingredient.name}>{ingredients}</option>;
});
return (
<div className="sidebar">
<label>
By ingredient:
<select>{displayIngredients}</select>
</label>
</div>
);
};
export default Sidebar
First, here
setIngredients(res.data);
change res.data to res.ingredients (the response object doesn't have data property). Then you'll face another bug,
const displayIngredients = ingredients.map((ingredient) => {
setIngredients(ingredient.name);
//...
First, ingredient.name is undefined, and second, it probably would be a string if it existed. Just ditch the setIngredients call here.
You are declaring displayIngredients as a variable typeof array (By directly affecting the array.map() result). You need it to be a function that return an array as follow :
const displayIngredients = () => ingredients.map((ingredient) => {
// Do not erase your previous values here
setIngredients(previousState => [...previousState, ingredient.name]);
// Changed it here as well, seems more logic to me
return <option key={ingredient.name}>{ingredient.name}</option>;
});
You should also wait for the API call to end before to display your select to prevent a blank result while your data load (If there is a lot). The easiest way to do that is returning a loader while the API call is running :
if(!ingredients.length) {
return <Loader />; // Or whatever you want
}
return (
<div className="sidebar">
<label>
By ingredient:
<select>{displayIngredients}</select>
</label>
</div>
);

Resources