React: adding objects to state array - javascript

I am trying to add multiple objects to a state array using a forEach loop:
const App = () => {
const [pokemon, setPokemon] = useState([]);
axios.get('https://pokeapi.co/api/v2/pokemon') // Gives me 20 pokemon
.then(res => {
res.data.results.map(p => p.url).forEach(url => { // Loops over endpoints of pokemon
axios.get(url)
.then(response => {
setPokemon(response.data)
})
});
})
}
As you can probably guess, only the last item from the forEach loop is shown when I console.log pokemon as it's the last one to be set in the state.
The api I'm working with: https://pokeapi.co/.
I start with making a call to https://pokeapi.co/api/v2/pokemon which gives me 20 pokemon, each pokemon object comes with an endpoint to get more information about it and it's the information from these endpoints I want to store as objects in the state.
Thanks for any help, if you know of a better way I can do this feel free to let me know.

I suggest you use Promise.all() inside of a useEffect() hook. By using useEffect, you can run your fetch code once when the component mounts (not each time it renders). Using Promise.all() allows you to pass an array of promises (which is what axios.get() returns), and resolve each Promise to one. This one Promise can resolve to an array of responses that each axios.get() call resulted in. This allows you to only set your state once.
See working example:
const {useState, useEffect} = React;
const App = () => {
const [pokemon, setPokemon] = useState([]);
useEffect(() => {
axios.get('https://pokeapi.co/api/v2/pokemon')
.then(res => Promise.all(res.data.results.map(pokemon => axios.get(pokemon.url))))
.then(arr => setPokemon(arr.map(response => response.data)));
}, []);
return (<div>
{pokemon.map(({id, species}) => <p key={id}>{species.name}</p>)}
</div>);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" crossorigin="anonymous"></script>

Just create a temporary array, add all the data which you get from the API to that, and later set the state using that variable. You don't have to set the state every time - you can set it at once.
const App = () => {
const [pokemon, setPokemon] = useState([]);
const tempArr = [];
axios.get('https://pokeapi.co/api/v2/pokemon') // Gives me 20 pokemon
.then(res => {
res.data.results.map(p => p.url).forEach(url => { // Loops over endpoints of pokemon
axios.get(url).then(response => {
tempArr.push(response.data);
})
});
});
setPokemon(tempArr);
}

Try setPokemon([...pokemon , response.data] )

You should probably use
setPokemon (pokemon.concat(response.data)

Related

How to fetch and manipulate data right when component mounts?

im having a little issue trying to fetch and filter some data when a component mounts. Basically what I am trying to do is to fetch some random data and then filter it with a condition. but the filtering of the data is not working, there is most likely a part I misunderstood using useEffect. I made a code sample where I simplified and replicated the issue on https://codesandbox.io/s/green-night-rhg4lj?file=/src/App.js
When I press on the button I expect the filtered data to be console logged, but gives me only an empty array, Ive tried to add "filteredData" or "fetchedData" as a dependency of the useEffect, and yes, it does help me getting the filtered data right at the start but goes into an endless loop because of the behaviour of the useEffect dependencies with obj and arrays. Anyone knows of a way to get the data from API/Database and filter it right on the mount without going into a fetch loop?
Here is also the code written beside the codesandbox:
import axios from "axios";
import { useState, useEffect, useCallback } from "react";
export default function App() {
const [fetchedData, setFetchedData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const getData = useCallback(async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
setFetchedData(data);
}, []);
useEffect(() => {
getData();
(async () => {
await setFilteredData(fetchedData.filter((p) => p.title.length > 20));
})();
}, []);
const clickHandler = () => {
console.log(filteredData);
};
return (
<div className="App">
<button onClick={clickHandler}>Click</button>
</div>
);
}
You almost right!
You were right when putting the request in the useEffect hook.
...
const getData = useCallback(async () => {
const { data } = await axios.get("https://jsonplaceholder.typicode.com/posts");
return data
}, []);
useEffect(async () => {
const dataFromAPI = await getData();
setFilteredData(dataFromAPI.filter((p) => p.title.length > 20));
}, []);
...
Instead updating the state in the getData funtion just return it.
In the useEffect you get that data and the do what ever you want to do there.
note:
According to this it's ok to use async in useEffect

Initialize state with async function data NextJS? [duplicate]

My component relies on local state (useState), but the initial value should come from an http response.
Can I pass an async function to set the initial state? How can I set the initial state from the response?
This is my code
const fcads = () => {
let good;
Axios.get(`/admin/getallads`).then((res) => {
good = res.data.map((item) => item._id);
});
return good;
};
const [allads, setAllads] = useState(() => fcads());
But when I try console.log(allads) I got result undefined.
If you use a function as an argument for useState it has to be synchronous.
The code your example shows is asynchronous - it uses a promise that sets the value only after the request is completed
You are trying to load data when a component is rendered for the first time - this is a very common use case and there are many libraries that handle it, like these popular choices: https://www.npmjs.com/package/react-async-hook and https://www.npmjs.com/package/#react-hook/async. They would not only set the data to display, but provide you a flag to use and show a loader or display an error if such has happened
This is basically how you would set initial state when you have to set it asynchronously
const [allads, setAllads] = useState([]);
const [loading, setLoading] = useState(false);
React.useEffect(() => {
// Show a loading animation/message while loading
setLoading(true);
// Invoke async request
Axios.get(`/admin/getallads`).then((res) => {
const ads = res.data.map((item) => item._id);
// Set some items after a successful response
setAllAds(ads):
})
.catch(e => alert(`Getting data failed: ${e.message}`))
.finally(() => setLoading(false))
// No variable dependencies means this would run only once after the first render
}, []);
Think of the initial value of useState as something raw that you can set immediately. You know you would be display handling a list (array) of items, then the initial value should be an empty array. useState only accept a function to cover a bit more expensive cases that would otherwise get evaluated on each render pass. Like reading from local/session storage
const [allads, setAllads] = useState(() => {
const asText = localStorage.getItem('myStoredList');
const ads = asText ? JSON.parse(asText) : [];
return ads;
});
You can use the custom hook to include a callback function for useState with use-state-with-callback npm package.
npm install use-state-with-callback
For your case:
import React from "react";
import Axios from "axios";
import useStateWithCallback from "use-state-with-callback";
export default function App() {
const [allads, setAllads] = useStateWithCallback([], (allads) => {
let good;
Axios.get("https://fakestoreapi.com/products").then((res) => {
good = res.data.map((item) => item.id);
console.log(good);
setAllads(good);
});
});
return (
<div className="App">
<h1> {allads} </h1>
</div>
);
}
Demo & Code: https://codesandbox.io/s/distracted-torvalds-s5c8c?file=/src/App.js

How to set a state value to an array in react native?

I have a functional component, ListKeys. When it loads I want to set an empty array of keys equal to a list of all keys extracted from storage. Here is what I've got at the moment:
const ListKeys = props => {
const [keys, setKeys] = useState([]);
const [areKeysLoaded, setAreKeysLoaded] = useState(false);
useEffect(() => {
if(!areKeysLoaded){
loadSavedKeys();
setAreKeysLoaded(true);
console.log(keys)
}
});
async function loadSavedKeys(){
try {
var allKeys = await AsyncStorage.getAllKeys();
console.log(allKeys);
setKeys(allKeys);
}
catch {
console.log("Error: Cannot access saved data.");
}
}
return (
<View></View>
);
};
export default ListKeys;
This code correctly gets the list of keys and outputs it to the console. This is done on line 16: console.log(allKeys);
However, when I then setKeys(allKeys);, this doesn't work. I know this because line 9: console.log(keys) outputs an empty array.
I'm guessing I can't just set a state value array to another array but I'm not experienced enough with JS or React Native to know why.
Can someone tell me how to properly set the keys array to the allKeys array?
You need to wait for the loadSavedKeys to resolve first before trying to set since it is async:
const ListKeys = props => {
const [keys, setKeys] = useState([]);
const [areKeysLoaded, setAreKeysLoaded] = useState(false);
useEffect(() => {
if (areKeysLoaded) return
AsyncStorage
.getAllKeys()
.then((keys) => {
setAreKeysLoaded(true)
setKeys(keys)
})
.catch(e => console.error(e));
}, []);
return (
<View></View>
);
};
export default ListKeys;
Note I simplified the code a bit since it seemed a little more verbose then necessary. They key take away is you need to wait for getAllKeys to resolve before you'll get the keys since it is asynchronous.
Also, you prob don't need areKeysLoaded if you just want this to run once, but I guess you could use it instead for a loading indicator?

useState on React Hooks not updating Array

I have tried many things and can't seem to understand why setTypes won't update the 'types' array??
import { useState, useEffect } from 'react';
import { PostList } from './post-list';
import * as api from '../utils/api';
export const PostSelector = (props) => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(false);
const [type, setType] = useState('post');
const [types, setTypes] = useState([]);
const fetchTypes = async () => {
setLoading(true);
const response = await api.getPostTypes();
delete response.data.attachment;
delete response.data.wp_block;
const postTypes = response.data;
console.log(response.data); // {post: {…}, page: {…}, case: {…}}
setTypes(postTypes);
console.log(types); // []
// Why types remain empty??
}
const loadPosts = async (args = {}) => {
const defaultArgs = { per_page: 10, type };
const requestArgs = { ...defaultArgs, ...args };
requestArgs.restBase = types[requestArgs.type].rest_base; // Cannot read property 'rest_base' of undefined
const response = await api.getPosts(requestArgs);
console.log(response.data);
}
useEffect(() => {
fetchTypes();
loadPosts();
}, []);
return (
<div className="filter">
<label htmlFor="options">Post Type: </label>
<select name="options" id="options">
{ types.length < 1 ? (<option value="">loading</option>) : Object.keys(types).map((key, index) => <option key={ index } value={ key }>{ types[key].name }</option> ) }
</select>
</div>
);
}
Please, take a look at the console.log and notice the different responses.
What I am trying to do is to load list of types, in this case 'post', 'page' and 'case' and then render a list of posts based on the current 'type'. The default type is 'post'.
If I add [types] to useEffect. I finally get the values but the component renders nonstop.
Thanks to everyone for your comments. Multiple people have pointed out the problem, being that, the fact that we set the state doesn't mean it will set right away because it it asynchronous.
How do we solve this problem then? Regardless of the reasons, how do we get it done? How do we work with our state at any point in time and perform calculations based on our state if we don't know when it will become available? How do we make sure we wait whatever we need to and then use the values we expect?
For any one coming here and not being able to set/update a useState array you need to use a spread operator (...) and not just the array e.g. "[...initState]" instead of "initState" ... in Typescript
//initialise
const initState: boolean[] = new Array(data.length).fill(false);
const [showTable, setShowTable] = useState<boolean[]>([...initState]);
// called from an onclick to update
const updateArray = (index: number) => {
showTable[index] = !showTable[index];
setShowTable([...showTable]);
};
It seems like useState is asynchronous and does not update the value instantly after calling it.
Review this same case here
useState's setTypes is an asynchronous function so the changes do not take effect immediately. You can use useEffect to check if anything changes
useEffect(()=>{
const defaultArgs = { per_page: 10, type };
const requestArgs = { ...defaultArgs, ...args };
requestArgs.restBase = types;
console.log("types updated",types)
},[types])
You can remove loadPosts because now useEffect will run whenever types change
You have declared your types to be an array, yet you are passing a dictionary of dictionaries through to it.
Try this:
const [types, setTypes] = useState({});
You also do not need to call
loadPosts()
becuase the useState hook will re-render your component, only updating what is needed.
Ok, The short answer is due to Closures
It not due to asynchronous as other answers said !!!
Solution (☞゚ヮ゚)☞
You can check the changes by console.log at return function like this.
return (
<div> Hello World!
{
console.log(value) // this will reference every re-render
}
</div>
);
or create a new one useEffect with value as a dependency like below
React.useEffect(() => {
console.log(value); // this will reference every value is changed
}, [value]);
function App() {
const [value, Setvalue] = React.useState([]);
React.useEffect(() => {
Setvalue([1, 2, 3]);
console.log(value); // this will reference to value at first time
}, []);
return (
<div> Hello World!
{
console.log(value) // this will reference every re-render
}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<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="app"></div>
Read here in more detail: useState set method not reflecting change immediately

Mapping over an array and returning API result

Trying a simple code to map over an array and, for each result, search an API for that result and spit out data retrieved from the API.
import React from 'react';
const App = () => {
let artistData = require('./mass-artists.json')
artistData.map(
(theArtist) => {
fetch(`[url]https://api.scryfall.com/cards/search?q=a:[/url]"${theArtist}"`)
.then((response) => {
return response.json();
})
.then((myJson) => {
console.log(JSON.stringify(myJson)); //returns the JSON data, so I know it's working
console.log(myJson.data[0].name); //returns the first card name
})
}
)
return(<span>This is here so React doesn't yell at me about no return statement.</span>)
}
export default App;
The JSON file is just a list of names of Magic artists. The map maps over the list of artists and searches on the API for them. I want to do the usual thing I do with the map, i.e. display something on the front-end. But I'm having trouble getting anything to work. I can log things to the console or do any Javascript function such as setting something's innerHTML, but where/how would I put a return statement to show the result in, say, a span tag? Even such as "Hello!".
Thanks as always!
I recommend looking into either the new hooks api or old Class components in react if you need more help with state. The following code will search for the data and then set state so that the component will know to update.
You also probably don't want to run that map directly in render because that will rerun your fetches each time the component updates.
import React, { useState, useEffect } from "react";
const App = () => {
const [artistsInfo, setArtistsInfo] = useState([]);
useEffect(async () => {
let artistData = require("./mass-artists.json");
const promises = artistData.map(theArtist => {
return fetch(
`[url]https://api.scryfall.com/cards/search?q=a:[/url]"${theArtist}"`
)
.then(response => {
return response.json();
})
.then(myJson => {
return myJson.data;
});
});
const results = await Promise.all(promises);
// results is an array of arrays because that is what your data seemed to be
// you could use lodash flatten if you want to get a single array
setArtistsInfo(results);
}, []); // empty array so it only runs once
// you could have a second useState to add a loader if you want
return artistsInfo.map(infoArray => {
return infoArray.map(singleInfo => {
return <span key={singleInfo.id}>{singleInfo.name}</span>;
});
});
};
export default App;

Categories