I am new in frontend application development and trying to populate a Select list from database, but cannot set options using the following approach using similar ones to How to populate select dropdown elements with data from API - ReactJS
const [options, setOptions] = useState([]);
useEffect(() => {
async function fetchData() {
const results = []
// Fetch data
GetWithAuth("/categories")
.then(res => res.json())
.then(value => {
value.map(element => {
// --> the value has data and push them to results variable
results.push({
key: element.name,
value: element.id,
});
});
})
// --> options cannot be filled with results and just have the following value
setOptions([
{key: 'Select a category', value: ''},
...results
])
}
// Trigger the fetch
fetchData();
}, []);
Here is the service method that returns Promise:
export const GetWithAuth = (url) => {
var request = fetch("/api" + url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: localStorage.getItem("tokenKey"),
},
});
return request;
};
So, how should I populate my select list from database? What is the problem with the code above?
You should update your state inside "then" function, also instead of using map function, you should use forEach (because you are not using return keyword), but if you want to use map function, then you can use it this way
const [options, setOptions] = useState([]);
useEffect(() => {
async function fetchData() {
// Fetch data
GetWithAuth("/categories")
.then(res => res.json())
.then(value => {
const results = value.map(element => {
// --> the value has data and push them to results variable
return {
key: element.name,
value: element.id,
}
});
setOptions([
{key: 'Select a category', value: ''},
...results,
])
})
}
// Trigger the fetch
fetchData();
}, []);
But if you want to use forEach function, then you can do it this way
const [options, setOptions] = useState([]);
useEffect(() => {
async function fetchData() {
// Fetch data
GetWithAuth("/categories")
.then(res => res.json())
.then(value => {
const results = [];
value.forEach(element => {
// --> the value has data and push them to results variable
results.push({
key: element.name,
value: element.id,
})
});
setOptions([
{key: 'Select a category', value: ''},
...results,
])
})
}
// Trigger the fetch
fetchData();
}, []);
Related
I have multiple api's that are returning same type of data but from different areas but the data type is the same only the values are different, and I want to store them all in one react state.
So I have this state:
let [infoData1, setInfoData1] = useState({ infoData1: [] });
let [infoData2, setInfoData2] = useState({ infoData2: [] });
and the axios calls :
function multipleApiCall() {
const headers = {
"X-Api-Key": "the-api-key-00",
};
axios.get(
"http:url-to-data/ID1",
{ headers }
)
.then((response) => {
setInfoData1(response.data);
return axios.get(
"http:url-to-data/ID2",
{ headers }
)
})
.then(response => {
setInfoData2(response.data);
})
}
and afterward I want to use a .map() to list the result but because I have 2 states I cannot concatenate them. So how can I have all data from those two api's in just one state or maybe another approach ?
const [infoData, setInfoData] = useState([]);
const headers = {
"X-Api-Key": "the-api-key-00",
};
const urls = ["http:url-to-data/ID1", "http:url-to-data/ID2"];
function multipleApiCall() {
const promises = urls.map(url => axios.get(url, { headers }));
Promise.all(promises).then(responses => {
let data = [];
responses.forEach(response => {
data = data.concat(response.data);
});
setInfoData(data);
});
}
I have a feedItems array. I want to set this array's items according to axios get request's response. In the screen, I want to show the information of the elements of this array. So, before everything, I must set this array so that I will be able to show the info of elements of this array.
I have no problem while making an API request and I am sure that the response.data is not empty. However, when I use the setFeedItems(...feedItems, response.data[i]); function, I get the following error.
[Unhandled promise rejection: TypeError: Invalid attempt to spread non-iterable instance.]
Here is the code:
const [feedItems, setFeedItems] = useState([{}]);
useEffect(async () => {
console.log("here");
let accessToken = await AsyncStorage.getItem("accessToken");
const response = await axios
.get(
"http://repor****-env-1.eba-nj*******/feed",
{
headers: {
Authorization: "Bearer " + accessToken,
},
}
)
.then((response) => {
for (let i = 0; i < response.data.length; i++) {
setFeedItems(...feedItems, response.data[i]);
}
//console.log(feedArray);
console.log(feedItems);
});
}, []);
The problem is that you're spreading out [{}] into discrete arguments to setFeedItems. To append response.data to feeditems, you do this:
.then((response) => {
setFeedItems(feedItems => [...feedItems, ...response.data]);
// No `console.log(feedItems)` here, it will show you outdated information
});
Notice:
Using the callback form, since you're updating state based on existing state, so you want to be sure to be using the up-to-date state.
The [] around the return value so you're creating an array and spreading the items out into it.
That it's spreading both the old feedItems and the new response.data out into that new array.
But if you want to replace feedItems with the data from response.data (which it looks like you probably do in this specific case), it's simpler:
.then((response) => {
setFeedItems(response.data);
// No `console.log(feedItems)` here, it will show you outdated information
});
Also, in the normal case, your feedItems would start out with an empty array, not an array with an empty object in it. So:
const [feedItems, setFeedItems] = useState([]);
Separately: useEffect won't do anything useful with the promise an async function returns (and does look at the return value, to see if it's a function), so you shouldn't pass an async function into it. Since you're already using explicit promise callbacks, there's no reason for the await and the async function. Also, you should handle promise rejection.
Putting all of that together (using the "replace" rather than "append" option):
const [feedItems, setFeedItems] = useState([{}]);
useEffect(() => {
AsyncStorage.getItem("accessToken").
then(accessToken => axios.get(
"http://repor****-env-1.eba-nj*******/feed",
{
headers: {
Authorization: "Bearer " + accessToken,
},
}))
.then((response) => {
setFeedItems(feedItems => [...feedItems, ...response.data]);
})
.catch(error => {
// ...handle/report the fact an error occurred...
});
}, []);
Live Example:
const { useState, useEffect } = React;
// Mock `AsyncStorage`
const AsyncStorage = {
getItem() {
return Promise.resolve("some token");
},
};
// Mock `axios`
const axios = {
get() {
return new Promise(resolve => {
setTimeout(() => {
resolve({data: [
{id: 1, text: "Item 1"},
{id: 2, text: "Item 2"},
{id: 3, text: "Item 3"},
{id: 4, text: "Item 4"},
]});
}, 800);
});
},
};
const Example = () => {
const [feedItems, setFeedItems] = useState([]);
useEffect(() => {
AsyncStorage.getItem("accessToken").
then(accessToken => axios.get(
"http://repor****-env-1.eba-nj*******/feed",
{
headers: {
Authorization: "Bearer " + accessToken,
},
}))
.then((response) => {
setFeedItems(response.data);
})
.catch(error => {
// ...handle/report the fact an error occurred...
});
}, []);
return <div>
<div>Item count: {feedItems.length}</div>
{feedItems.map(({id, text}) => <div key={id}>{text}</div>)}
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
OK, there are a couple of issues with your code:
useEffect shouldn't use async functions. You should create an async function inside and call it there.
you are using await and then. Should stick to one.
you are actually adding a single element here setFeedItems(...feedItems, response.data[i]); instead of an array.
Try to use this
setFeedItems([...feedItems, response.data[i]]);
This will fix the error, but it won't fix your problem, as you are getting old feedItem.
Why not updating the state with all the items inside response.data at once?
Instead of
for (let i = 0; i < response.data.length; i++) {
setFeedItems(...feedItems, response.data[i]);
}
just do:
if (response.data.length) setFeedItems(prevState => [...prevState, ...response.data])
After fetching some date from external API using this code
let fetchedData = [];
fetch(api)
.then(res => res.json())
.then(res => JSON.parse(res.response))
.then(arr => arr.map(e => fetchedData.push(e)) )
.catch(err => err);
fetchedData is populated without any problem, at that moment data inside look like this
{
Key: '0',
Record: {
a: 'a0',
b: 'b0'
}
},
{
Key: '1',
Record: {
a: 'a1',
b: 'b1'
}
},
{
Key: '2',
Record: {
a: 'a2',
b: 'b2'
}
}
for console.log(fetchedData) chrome tools display something like that:
[]
0: {Key: "0", Record: {…}}
1: {Key: "1", Record: {…}}
2: {Key: "2", Record: {…}}
length: 3
__proto__: Array(0)
but if I want to access and extract this array like console.log(fetchedData[0])
then i shows as undefined
or console.log(fetchedData.length) shows 0
but in the same script next call to console.log(fetchedData) shows normal array like previously
as a result in chrome tools i have
array
undefined
array
using code mentioned earlier
The reason this is happening is because you are doing an asynchronous operation, fetch returns a promise, so async means it will run after sync code runs: in this example it will run after js let fetchedData = []; runs. if that makes sense.
To handle this situation you could call this fetch operation inside a
js componentDidMount() {} or js useEffect(() => {})
This is an example, but try it #wokadakow with your data and check the result!
function App() {
const [data, setData] = useState([]);
async function AsyncData() {
// Here goes your async data, just replace it
// this is fake data
return await [{
id: 1,
name: 'john'
}]
}
useEffect(() => {
AsyncData()
.then((data) => setData(data))
}, [])
return (
<div className="App">
<h1> TEST DATA </h1>
<ul>
{data.map(d => {
return <li key={d.id}> {d.name} </li>
})}
</ul>
</div>
)
}
render(<App />, document.getElementById('root'));
I hope this helps!
You will have to wait until your fetch is done either by using async/await or by adding your console.log call into another then method:
let fetchedData = [];
fetch(api)
.then(res => res.json())
.then(res => JSON.parse(res.response))
.then(arr => arr.map(e => fetchedData.push(e)) )
.then(arr => console.log(arr) )
.catch(err => err);
I'm relatively new to React and I'm trying to set state using data that I'm receiving from a fetch response. To do this, I'm chaining a function that gathers the data needed, adds it to a state object and uses setState to add it toState and then do the same to the next response that comes in.
My problem is that each attempt is getting overwritten by the next, meaning only the last response is processed and added to sate.
What am I not doing, or doing incorrectly, that's causing this?
I've already tried using prevState however this breaks the page and produces an error: "Invalid attempt to spread non-iterable instance"
buildDataObject = (str, arr) => {
this.setState(prevState => [...prevState, {
name: str,
labels: arr.map(obj => obj.description),
id: arr[0].mid
}]);
}
Here's my state before the script runs:
constructor(){
super();
this.state = {
images: []
}
}
On componentDidMount, I run a fetch request for each image in an array:
componentDidMount() {
images.forEach(index => this.getLabels(index));
}
Here's the fetch request:
getLabels = (path) => {
const url = getGoogleVisionUrl();
fetch((url), {
method: 'POST',
body: JSON.stringify(createRequestJSON([path]))
}).then(response => response.json())
.catch((err) => { console.log('error!', err); })
.then(data => data.responses[0].labelAnnotations)
.then(arr => this.buildDataObject(path, arr));
}
Which calls on a function that's supposed to process each response and add it to state:
buildDataObject = (str, res) => {
this.setState([{
name: str,
labels: res.map(obj => obj.description),
id: res[0].mid
}]);
}
The state ends up as being a single object as:
{
name: / string response data /,
labels: /an array or strings/,
id: /an ID number/
}
if you don't specify they key that you want to update in the state it will add an object to it, and overwrite it everytime, you need to add the object to the images array in the state :
buildDataObject = (str, arr) => {
this.setState(prevState => ({
images: [
...prevState.images,
{
name: str,
labels: arr.map(obj => obj.description),
id: arr[0].mid
}
]
}));
};
I am trying to test the loadAllProjects function.
The test fails at .then() with the error: TypeError: Cannot read property 'then' of undefined
I have also tried mocking the reponse of getHeadersWithToken() but could not get it to work.
Snookered on this one and would appreciate any help.
test:
it('should create SET_ALL_PROJECTS action when fetching projects', () => {
fetch
.once(JSON.stringify([{ access_token: "12345" }]))
.once(JSON.stringify({ name: "x" }))
const expectedActions = [
{ type: "SET_ALL_PROJECTS", json: { name: "x" } },
]
store.dispatch(actions.loadAllProjects.apply())
.then(() => { // FAILS HERE
expect(store.getActions()).toEqual(expectedActions)
})
});
code:
export const getHeadersWithToken = () => {
return fetch("/.auth/me", requestOptions)
.then(parseResponseAndHandleErrors)
.then(json => {
const header = 'Bearer ' + json[0].access_token
const applicationJsonHeaders = getJsonHeaders(header)
return applicationJsonHeaders
})
.catch( error=> {
console.error(error)
})
}
export const loadAllProjects = () => {
return (dispatch) => {
getHeadersWithToken()
.then(applicationJsonHeaders => {
const requestOptions = {
method: 'GET',
headers: applicationJsonHeaders,
};
return fetch(process.env.REACT_APP_PROJECTS_API_URL + "/projects", requestOptions)
.then(parseResponseAndHandleErrors)
.then(json => {
dispatch(setAllProjects(json))})
.catch(error => {
console.error(error)
dispatch(failedToLoadProjects(error))
});
})
}
}
store used in test:
const store = mockStore(Map(
{
allProjects: Map({
}),
currentProject: Map({
authenticationData: Map({
})
})
})
);
What Redux middleware are you using for the async stuff? Make sure you set up the middleware when creating the store for testing.
Since i dont see that anywhere in your code above im gonna assume we are not using that middleware here.
Since loadAllProjects is a higher order function i would do this:
it('should create SET_ALL_PROJECTS action when fetching projects', (done) => {
fetch
.once(JSON.stringify([{ access_token: "12345" }]))
.once(JSON.stringify({ name: "x" }))
const expectedActions = [
{ type: "SET_ALL_PROJECTS", json: { name: "x" } },
]
// Higher order function that returns a new function.
const loadAllProjectsAsync = actions.loadAllProjects();
// The function returned expects a dispatch from Redux as an argument.
// It will do async work and when its done, it will call the provided dispatch.
loadAllProjectsAsync(store.dispatch).then(() => {
expect(store.getActions()).toEqual(expectedActions);
done();
})
});
You also need to modify your code for loadAllProjects so that the inner function returns the promise:
export const loadAllProjects = () => {
return (dispatch) => {
// You will need to return the promise in order for the test to be able to call .then() on it.
return getHeadersWithToken()
.then(applicationJsonHeaders => {
const requestOptions = {
method: 'GET',
headers: applicationJsonHeaders,
};
return fetch(process.env.REACT_APP_PROJECTS_API_URL + "/projects", requestOptions)
.then(parseResponseAndHandleErrors)
.then(json => {
dispatch(setAllProjects(json))})
.catch(error => {
console.error(error)
dispatch(failedToLoadProjects(error))
});
})
}}
Also, as already stated, you must tell the jest when the test is done if you are testing async stuff. Do this by letting your it call take done as a param and call that as a function after you have verified the outcome i the .then()
This was just a quick fix from my side. There still might be something im missing or some bugs in the code above, but you get the point.
Let me know if you have any follow up questions?
In case this is ever useful to anyone and in acknowledgment of Septastium's answer, I eventually changed the code to:
async getHeadersWithToken(requestType) {
if (process.env.REACT_APP_RUNNING_LOCALLY==="true") {
return {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
};
}
let result = await fetch("/.auth/me", this.requestOptions)
let headers = result.json()
.then( json => {
const header = 'Bearer ' + json[0].access_token
const applicationJsonHeaders = this.getJsonHeaders(header, requestType)
return applicationJsonHeaders
})
.catch(error => {
console.error(error)
})
return headers
}
export const loadAllProjects = () => {
return async dispatch => {
const authenticator = new Authenticator()
let applicationJsonHeaders = await authenticator.getHeadersWithToken(constants.GET)
let loggedInUser = await authenticator.getLoggedInUser()
const requestOptions = {
method: 'GET',
headers: applicationJsonHeaders,
};
return await fetch(process.env.REACT_APP_PROJECTS_API_URL + "/projects", requestOptions)
.then(response => {
return parseResponseAndHandleErrors(response)
})
.then(json => dispatch(setAllProjects(json)))
.then(()=> dispatch(setAuthenticationData(loggedInUser)))
.catch(error => {
console.error(error)
return dispatch(failedToLoadProjects(error))
});
}
}
and the test to:
const checkActionsWereDispatched = async (expectedActions, actionCreator) => {
const store = mockStore(Map(
{
}),
);
store.dispatch(await actionCreator.apply()).then(() => {
expect(store.getActions()).toEqual(expectedActions)
})
}
it('should create SET_ALL_PROJECTS action when fetching projects', async () => {
fetch
.once(JSON.stringify([{ access_token: "12345" }]))
.once(JSON.stringify({ name: "x" }))
const expectedActions = [
{ type: "SET_ALL_PROJECTS", json: { name: "x" } },
]
checkActionsWereDispatched(expectedActions, actions.loadAllProjects)
});
As noted above I think Spetastium's version of the test is easier to read than mine and his article here was very helpful.