I have a method in a React component which fetches some data from an API
this.state.matches returns an empty array at first
loadMatches() {
let matches = this.state.matches;
forEach(this.state.matchIds.splice(0,5), matchid => {
axios.get(url)
.then(function (response) {
matches.push(response.data)
})
});
this.setState({
matches
})
}
And then a method which should map the data to React components
renderMatch() {
return this.state.matches.map((match, index) => {
return (
<Match
key={index}
gameId={match.gameId}
/>
);
});
}
renderMatch() is called in my render method with {this.renderMatch()}
But nothing is getting rendered and if i call .length it just returns 0 even though i can see in devtools that the array contains 5 objects.
Hardcoded objects in the array gets rendered
You are mutating the state so React doesn't trigger a new render. You should create a new array instead of pushing in the state :
loadMatches() {
let promises = [];
forEach(this.state.matchIds.splice(0,5), matchid => {
promises.push(axios.get(url).then(res => res.data));
});
Promise.all(promises).then(matches => {
this.setState({
matches
});
});
}
Edited to handle the async.
axios or fetch is an async function, there for when you call this.setState matches is still an empty array, hence nothing is rendered. try this instead. also you are trying to mutate the state directly which is a big mistake.
loadMatches() {
let matches = this.state.matches;
let newMatches =[];
let requests = forEach(this.state.matchIds.splice(0,5), matchid => {
return new Promise((resolve) => {
axios.get(url)
.then(function (response) {
newMatches.push(response.data)
})
});
});
Promise.all(requests).then(() => {
this.setState({
matches:newMatches
})
});
}
You have two main problems:
First:
.then(function (response) {
matches.push(response.data)
})
You should not change state directly, you should perform immutable update.
Second:
this.setState({
matches
})
You are updating state with empty 'matches', at this point your api call's responses are not received yet.
So, your implementation should look like this:
loadMatches() {
let matches = this.state.matches;
forEach(this.state.matchIds.splice(0,5), matchid => {
axios.get(url)
.then(function (response) {
const newMatch = response.data;
this.setState({ matches: [...matches, newMatch]});
matches.push(response.data)
})
});
}
Here some sources that you can also benefit from reading them:
https://daveceddia.com/immutable-updates-react-redux/
https://reactjs.org/docs/state-and-lifecycle.html
axios.get is an asynchronous function which returns a promise. Its like you've ordered a pizza and trying to eat it before it gets delivered. You need to perform the setState() when you resolve the promise.
axios.get(url)
.then(function (response) {
this.setState({matches: [...matches, ...response.data]})
}
Related
I am learning javascript, trying out some things, but currently having an issue.
I am trying to work with an API. Essentially - the API endpoint returns an array and within the indexes there is the data in identical structure across each index element.
So for example, it looks like this:
0-1000
Then when I expand it out, they're like this:
0:
param_1: 1
param_2: 2
1:
param_1: 3
param_2: 4
2:
param_1: 5
param_2: 6
... and so on.
The data structure, as shown by the DevTools:
The problem is - when I run the below code, I can print to console the top level array which returns the arrays named 0, 1, 2, 3, etc. OR I can also obviously give an array element and say console.log(array[45]).
But what I want to do is do a foreach to remove the top level index if this makes sense and just return the same parameter from each sub-array element. So for example, just return each and every param_1 into a new array or to a console log.
My problem is, I've tried many things and I do not have enough understanding of all the complexities to make this work. I've tried a .map, or a for-each, but I think there are issues with promises not being fulfilled, etc. I also don't want to do a for-each on the whole of the API fetch which returns the original array.
The below code is what I'm working with and what works, and I will add in pseudocode further down what my intention is.
Working:
export const getWeatherData = (location) => {
fetch(url, opts)
.then(function (response) {
document.querySelector(".loading").textContent = "";
return response.json();
})
.then(function (response) {
return response;
})
.then(function (response) {
const weatherData = {
name: response.data.blah
};
console.log(weatherData.name);
return weatherData;
})
.then(function (weatherData) {
displayWeatherData(weatherData);
})
.catch(function (err) {
err = displayError();
});
};
Pseudocode:
export const getWeatherData = (location) => {
fetch(url, opts)
.then(function (response) {
document.querySelector(".loading").textContent = "";
return response.json();
})
.then(function (response) {
return response;
})
.then(function (response) {
for each (item in response){
add item.data.blah.param_1 to newarray.
}
const weatherData = newarray
console.log(weatherData);
return weatherData;
})
.then(function (weatherData) {
displayWeatherData(weatherData);
})
.catch(function (err) {
err = displayError();
});
};
I hope this makes sense, any questions, etc. or if you need clarity I will respond ASAP.
const newarray = response.map((item) => {
return item.data.blah.param_1;
});
This sould work for your problem. If there is sub arrays u can use forEach like this
const newarray = [];
response.forEach((item) => {
item.data.blah.forEach((subItem) => {
newarray.push(subItem.param_1);
});
});
console.log(newarray);
My app has a search bar where the user types in a word and clicks search. Upon click, the app fetches the definition of that word from an open dictionary API, and then updates the parent component's state to reflect the results. The component is then supposed to render the results by passing that as a prop to a presentational child component.
However, it looks like the state gets set before the fetch call has the time to return the data. So the search results are not rendered until the search button is clicked again, causing confusion.
I have tried resolving this by making my function asynchronous (Dictionary.search refers to an imported function which handles the fetch and returns the result in the form of an array):
async search(term) {
const results = await Dictionary.search(term);
this.setState({ searchTerm: term, results: results });
}
However this only works some of the time. For longer searches, it doesn't wait before updating the state and re-rendering.
I have done some googling and came across this suggestion, but it also doesn't work:
search = (term) => {
const dictionarySearch = async (term) => {
const results = await Dictionary.search(term);
this.setState({ searchTerm: term, results: results });
};
dictionarySearch(term);
};
EDITED TO ADD: Here is the Dictionary.search code, along with its helper function:
//Create an empty array in which the results of the API call will be stored
let results = [];
const Dictionary = {
search(term) {
//Url with our search term and API key:
const url = `https://www.dictionaryapi.com/api/v3/references/collegiate/json/${term}?key=${api_key}`;
//Fetch the results from Merriam Webster API:
fetch(url)
.then((response) => {
//Catch an error from their server
if (!response.ok) {
throw new Error("Network response was not ok");
}
//Return javaScript object version of the response
return response.json();
})
.then((jsonResponse) => {
//Perform the helper function on the javascript object and return the result (array)
return shortDef(jsonResponse);
})
//Catch any other errors than a server error
.catch((error) => {
console.error(
"There has been a problem with your fetch operation:",
error
);
});
//Create a copy of the results array
let returnResults = results.slice();
//Reset the original array to an empty array
results = [];
//Return the copy
return returnResults;
},
};
//Helper function to extract only certain info from the API
function shortDef(response) {
response.forEach((object) => {
//Create a new object for each object int he response array
let result = { word: "", type: "", definitions: [] };
//Add the word and type to the object
result.word = object.hwi.hw;
result.type = object.fl;
//Add the definitions to the object. There may be several, so it is pushed to an array.
let defs = object.shortdef;
defs.forEach((def) => {
result.definitions.push(def);
});
//Add the object to the array of API results
results.push(result);
});
//Return the list of results
return results;
}
I don't want to call the API in the ComponentDidMount, because it should get called every time the user presses "search". I also would prefer not to use useEffect, as it would mean refactoring my entire component from a class to a function.
Is there no way to have the setState in a class component wait for an asynchronous task to complete?
The problem is that your Dictionary.search function immediately returns, because it does not wait until the .then block resolves. Change it to an async function and await the fetch of the url. It should look like this:
const Dictionary = {
// Make search an async function
search: async term => {
const url = `https://www.dictionaryapi.com/api/v3/references/collegiate/json/${term}?key=${api_key}`;
// Await the results
await fetch(url)
.then(response => {
// ...
})
.then(jsonResponse => {
// ...
})
.catch(error => {
// ...
});
return;
},
};
I have two APIs reporting two sets of data (lockboxes and workstations). The lockboxes API has a collection of agencies with a recordId that I need to manipulate. The workstations API is the main collection that will assign one of these agencies (lockboxes) on a toggle to a workstation by sending the lockboxes.recordId and the workstation.recordId in the body to the backend.
My store looks like this
import { axiosInstance } from "boot/axios";
export default {
state: {
lockboxes: [],
workstation: []
},
getters: {
allLockboxes: state => {
return state.lockboxes;
},
singleWorkstation: state => {
let result = {
...state.workstation,
...state.lockboxes
};
return result;
}
},
actions: {
async fetchLockboxes({ commit }) {
const response = await axiosInstance.get("agency/subagency");
commit("setLockboxes", response.data.data);
},
updateAgency: ({ commit, state }, { workstation, lockboxes }) => {
const postdata = {
recordId: state.workstation.recordId,
agency: state.lockboxes.recordId
};
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("assignAgency", workstation);
console.log(state);
}
})
.catch(({ error }) => {
reject(error);
});
});
}
},
mutations: {
setWorkstation: (state, workstation) => (state.workstation = workstation),
assignAgency(workstation) { workstation.assign = !workstation.assign},
setLockboxes: (state, lockboxes) => (state.lockboxes = lockboxes)
}
};
Process:
When I select a lockbox from the dropdown and select a toggle switch in the workstation that I want to assign the lockbox too, I do get the lockbox to show but it goes away on refresh because the change only happened on the front end. I'm not really passing the workstation.recordId or lockboxes.recordId in my body as I hoped I was. It is not reading the state and recognizing the recordId for either state(workstation or lockboxes).
the console.log is returning (Uncaught (in promise) undefined)
The request is 404ing with an empty Payload in the body ( {} )
Not even the mutation is firing
template
toggleAssign(workstation) {
this.updateAgency(workstation);
}
At some point I had it that is was reading the workstation.recordId before I tried to merge the two states in the getter but I was never able to access the lockboxes.recordId. How can I have access to two states that live in two independent APIs so I can pass those values in the body of the request?
You can add debugger; in your code instead of console.log to create a breakpoint, and inspect everything in your browser's debug tools.
I can't really help because there are very confusing things:
state: {
lockboxes: [],
workstation: []
},
So both are arrays.
But then:
setWorkstation: (state, workstation) => (state.workstation = workstation),
assignAgency(workstation) { workstation.assign = !workstation.assign},
It seems that workstation is not an array?
And also this, in the getters:
singleWorkstation: state => {
let result = {
...state.workstation,
...state.lockboxes
};
return result;
}
I'm not understanding this. You're creating an object by ...ing arrays? Maybe you meant to do something like:
singleWorkstation: state => {
let result = {
...state.workstation,
lockboxes: [...state.lockboxes]
};
return result;
}
Unless lockboxes is not an array? But it's named like an array, it's declared as an array. You do have this however:
const postdata = {
recordId: state.workstation.recordId,
agency: state.lockboxes.recordId
};
So it seems it's not an array?
Finally, in your updageAgency method, and this is where the problem may lie:
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("assignAgency", workstation);
console.log(state);
}
})
.catch(({ error }) => {
reject(error);
});
});
The .then first arg of axios is only invoked if the status code is 2xx or 3xx. So your test if (status === 200) is superfluous because errors would not get there. And if for a reason of another you have a valid code other than 200, the promise never ends. reject is never called, as it's not an error, and neither is resolve. So you should remove check on the status code.
You should also call resolve(true) after the two commits, not before...
Finally your mutation assignAgency is declared all wrong:
assignAgency(workstation) { workstation.assign = !workstation.assign},
A mutation always takes the state as the first param. So it should either be:
assignAgency(state, workstation) {
state.workstation = {...workstation, assign: !workstation.assign}
},
Or
assignAgency(state) {
state.workstation = {...state.workstation, assign: !state.workstation.assign}
},
Depending on if you even need the workstation argument, given that what you want is just toggle a boolean inside an object.
TLDR: I'm not sure if lockboxes should be an array or an object, remove the status check inside your axios callback, fix the assignAgency mutation, use breakpoints with debugger; and the VueJS chrome plugin to help examine your store during development.
In an action, you get passed 2 objects
async myAction(store, payload)
the store object is the whole vuex store as it is right now. So where you are getting commit, you can get the state like so
async fetchLockboxes({ commit,state }) {//...}
Then you can access all state in the app.
You may use rootState to get/set whole state.
updateAgency: ({ commit, rootState , state }, { workstation, lockboxes }) {
rootState.lockboxes=[anything you can set ]
}
I'm writing a React application that fetches image data from a server for an array of URLs. I am storing the camera images as large strings that are placed into the image's src attribute. I am using useReducer to store my dictionary of camera objects.
I am having a couple of problems getting the reducer to work, and one of them has to do with some confusion I'm having with asynchronous values and why the async function returns correct output but the completion handler (.then()) receives undefined as a result.
Here is the code for useEffect() and the asynchronous fetching function.
useEffect()
//Why is cameras undefined?
useEffect(() => {
if (phase === 0) {
let cameras = {}
getCameraInformation().then((cameras) => {
debugger;
dispatch({
type: 'loadedCameraInformation',
payload: {cameras: cameras}
});
}).finally(() => setPhase(1))
}
});
My function signature and variables:
export default function Main() {
const [state, dispatch] = useReducer(cameraReducer, initialState);
let [phase, setPhase] = useState(0);
My function for getCameraInformation:
This returns a dictionary full of correct information!
async function getCameraInformation() {
//returns a json with the following: url, cam_name, cam_pass, cam_user, channel, chunk, group, path, port,
// uptime, username.
let cam_json = await axios
.get(getCamerasURL, { headers: { auth: get_cookie("token") } })
.then(response => {
let tempCameraArray = response.data.body;
let tempCameraDictionary = {};
for (var camera in tempCameraArray) {
tempCameraDictionary[tempCameraArray[camera].sid] = {
cameraInformation: tempCameraArray[camera],
cameraImage: null
};
}
return tempCameraDictionary;
})
.catch(error => console.log(error));
}
Your async function getCameraInformation doesn't have a return statement, so its promise will not resolve any value. There is a return in the then callback, but that's a different function entirely.
You are also using await and then() on the same promise, which isn't ideal. Use one or the other, because it's very easy to get confused when you mix and match here.
You already have an async, so don't use then at all in side that function.
async function getCameraInformation() {
//returns a json with the following: url, cam_name, cam_pass, cam_user, channel, chunk, group, path, port,
// uptime, username.
let response = await axios.get(getCamerasURL, { headers: { auth: get_cookie('token') } })
let tempCameraArray = response.data.body
let tempCameraDictionary = {}
for (var camera in tempCameraArray) {
tempCameraDictionary[tempCameraArray[camera].sid] = {
cameraInformation: tempCameraArray[camera],
cameraImage: null,
}
}
return tempCameraDictionary
}
I make HTTP request with axios and inside it I make another HTTP request in order to add some details about items that I get. I need to setState after I push it to the 'orders' Array, but it does it before so I can't print it in the correct way.
It works with SetTimeout but I want to do it in more professional way.
How can I do it synchronous??
fetchOrders(){
let orders = [];
let status;
this.setState({loading:true});
http.get('/api/amazon/orders')
.then(response => {
if (response.status === 200) status = 200;
orders = response.data;
orders.map(order => {
order.SellerSKU = "";
http.get(`/api/amazon/orders/items/${order.AmazonOrderId}`)
.then(res => {
order.SellerSKU = res.data[0].SellerSKU;
})
.catch(error => {
console.log(error);
})
});
setTimeout( () => {
this.setState({orders, error: status ? false : true, loading:false})
}, 1000);
})
.catch(error => {
this.setState({loading:false, error:true});
console.error(error);
});
}
You seem to be asking the wrong question, which is how to implement something async as sync, instead you really are trying to defer setting your state until everything is finished. (aka the XY Problem)
You don't need to make setState synchronous - you just need to leverage some promise chaining and Promise.all, which will allow you to defer the setState call until everything is finished.
In short you should be able to adapt this to something you need, applying a few more transformations to the responses:
fetchOrders() {
http.get('/first')
.then(r => r.data)
.then(orders => {
// This wraps the iterable of promises into another promise
// that doesn't resolve until all the promises in the iterable
// have finished.
return Promise.all(orders.map((order) => http.get(`/second/${order.id}`).then(/* transform */))
})
.then(transformedOrderPromises => this.setState({ ... });
}
setState can take a callback function, after finishing mutate the state it will execute the callback. So you can setState and add your 2nd API call in the callback.
Something like this:
http.get('/api/amazon/orders')
.then(response => {
if (response.status === 200) status = 200;
orders = response.data;
this.setState({orders, error: status ? false : true, loading:false},
() => {
orders.map(order => {
order.SellerSKU = "";
http.get(`/api/amazon/orders/items/${order.AmazonOrderId}`)
.then(res => {
order.SellerSKU = res.data[0].SellerSKU;
}).catch(error => {
console.log(error);
})
});
})
Please note that I just edited a dirty way, may be you need to make some adjustment to make it works.
In my projects, I fake a synchronous setState to avoid lot of pitfalls, making the code cleaner. Here's what I do:
class MyComponent extends React.Component {
// override react's setState
setState(partialState) {
const mergedState = { ...this.state, ...partialState };
this.state = mergedState;
super.setState(partialState);
}
}
Explained: before calling the true setState, the state is also set into this.state so that any reference to it before the actual update is correct.
The only downside is that you have to extend from MyComponent rather than React.Component.