React setState after Promise resolves in componentDidMount - javascript

I am still new to React and trying to wrap my head around.
I am fetching some data from an API in ProjectAPI.js file.
const getProjects = async () => {
const projectsAPI = Common.baseApiUrl + '/project';
let projects = [];
axios.get(projectsAPI)
.then((response) => {
for (let i = 0; i < response.data.length; i ++){
let project = {
projectNameInitials: "NO",
projectNumber: response.data[i].projectNumber,
projectName: response.data[i].projectName,
clientName: response.data[i].client,
currentStage: response.data[i].currentStage,
lastUpdated: response.data[i].updatedOn
}
projects.push(project);
}
})
.catch((error) => {
console.log(error);
});
return projects;
}
Then, in my React component I call this function and sets the state after the Promise resolves using then.
componentDidMount(){
ProjectAPI.getProjects().then((response) => this.setState({projects: response}));
}
I try to retrieve the same from the state in my render() function.
render(){
const {
projects,
} = this.state;
//...
}
This does not work and I get projects as empty array inside render() function. However, using the React dev tools, I can see the state is having the exact data. Interestingly, when I modify one of the state value manually using React dev tools, the render() is able to retrieve the state data, since it triggers the render() again. Any idea what I am doing wronng here?

Cause getProjects return empty array. Try this
const getProjects = () => {
const projectsAPI = Common.baseApiUrl + '/project';
let projects = [];
return axios.get(projectsAPI)
.then((response) => {
for (let i = 0; i < response.data.length; i ++){
let project = {
projectNameInitials: "NO",
projectNumber: response.data[i].projectNumber,
projectName: response.data[i].projectName,
clientName: response.data[i].client,
currentStage: response.data[i].currentStage,
lastUpdated: response.data[i].updatedOn
}
projects.push(project);
}
return projects
})
.catch((error) => {
console.log(error);
});
}

You just need to wait for data to come, because in this case render method getting called before your data comes from api.
So Just put condition before you render if state is null then render null else your html
render(){
const {
projects,
} = this.state;
{projects && 'your code' }
}

Related

How can I merge two arrays from Axios into one "datasource" array and pass it as a data into syncfusion SeriesDirective component?

the main problem is that when I console.log "datasource" array it first shows that array populated the way I want to, which is:
[
0:{ date: date, value: value },
1:{ date: date, value: value },
...
]
but then after the first log loads it suddenly changes to a empty array [];
Here is my code:
import React, {useState, useEffect} from 'react'
import { ChartComponent, LineSeries, ColumnSeries, SeriesDirective, SeriesCollectionDirective, Inject } from '#syncfusion/ej2-react-charts'
import axios from 'axios'
const StablesTVLchart = () => {
const [stables, setStables] = useState([])
const dates = new Array;
const totalCirculating = new Array;
const totalPegged = new Array;
const datasource = new Array;
useEffect(() => {
axios.get('https://stablecoins.llama.fi/stablecoincharts/all?stablecoin=1')
.then(res => {
setStables(res.data)
// Populate dates array and totalCirculating array /
for (var i = 0; i < stables.length; i++){
dates.push(parseFloat(stables[i].date))
totalCirculating.push(data[i].totalCirculatingUSD)
}
// Populate totalPegged array /
for (var y = 0; y < totalCirculating.length; y++){
totalPegged.push(totalCirculating[y].peggedUSD)
}
// Populate datasource array with date and totalPegged /
for (var e = 0; e < datadate.length; e++){
datasource.push({ date: dates[e], value: totalPegged[e] })
}
})
.catch(err => {
console.log(err)
})
}, []);
console.log(datasource);
const primaryxAxis = {visible: false }
const primaryyAxis = { labelFormat: '${value}K', visible: false }
const palette = ["skyblue"]
return (
<div className="w-full">
<ChartComponent id="charts" primaryXAxis={primaryxAxis} primaryYAxis={primaryyAxis} palettes= {palette}>
<Inject services={[ColumnSeries, LineSeries]} />
<SeriesCollectionDirective>
<SeriesDirective dataSource={datasource} xName='date' yName='value' name='TVL'/>
</SeriesCollectionDirective>
</ChartComponent>
</div>
)
}
export default StablesTVLchart
I recon that its possible I need to somehow make this datasource array into a state array?
If anyone has any clues or an idea on how to do this, I'd be very grateful.
As it appears from the code you provided that you are using state to store the data, but immediately trying to use it, I will remind you that React may batch multiple setState() calls into a single update for performance.
Also for improvement of the code readability, you can use something like:
useEffect(() => {
axios.get('https://stablecoins.llama.fi/stablecoincharts/all?stablecoin=1')
.then(res => {
const stables = res.data;
const dates = stables.map(item => parseFloat(item.date));
const totalCirculating = stables.map(item => item.totalCirculatingUSD);
const totalPegged = totalCirculating.map(item => item.peggedUSD);
const datasource = totalPegged.map((value, index) => ({ date: dates[index], value }));
setStables(datasource);
})
.catch(err => {
console.log(err)
});
}, []);
The api returns a single array. You store the api's response in a state you don't use. You then store parts of the data of original response in 4 arrays by mutating them (using push). You try to use one of the mutated arrays (datasource) in the view, but as soon as the view is re-rendered (because of setStables(res.data)) the array is re-created with an empty array. In addition, updating a variable/constant asynchronously, and mutating the arrays doesn't cause a re-render, and even if it would react won't detect the change, and won't change the view.
Solution:
Use Array.map() to create a new array based on the api's response array
Store result of the map into the state, and use that state in your view
Example:
const StablesTVLchart = () => {
const [datasource, setDatasource] = useState([])
useEffect(() => {
axios.get('https://stablecoins.llama.fi/stablecoincharts/all?stablecoin=1')
.then(({ data }) => {
setDatasource(data.map(({ date, totalCirculatingUSD }) => ({
date: parseFloat(date),
value: totalCirculatingUSD.peggedUSD
})))
})
.catch(err => {
console.log(err)
})
}, []);
console.log(datasource);
}

ReactJS / NextJS state array not rendering after setState

I am having trouble rendering my objects using .map() within React / NextJS.
I have a function where I get images from Firebase Cloud Storage, code below:
getImages = () => {
let firebase = loadFirebase()
var storageRef = firebase.storage().ref('chest')
let { state } = this
storageRef.listAll().then((result) => {
let data = []
result.items.forEach((imageRef) => {
imageRef.getDownloadURL().then((url) => {
data.push(url)
}).catch((error) => {
// Handle any errors
})
})
state.images = data
this.setState({ images: data })
}).catch((error) => {
// Handle any errors
})
}
This part seems to work as I do get data back and the state is updated, results as in screenshot:
Results after setState
I then map through images with the code below:
{ this.state.images.map((image, index) => {
return (
<img
key={ index }
src={ image }
alt=""
/>
)
})}
On the same page as this, I have other places where I get data from Firebase, set my states accordingly and render the objects using .map(). In those cases it works perfectly fine. Difference is that in those cases I use getInitialProps() to get my data from Firebase, whereas with the data from Cloud Storage I have a function, the getImages() function above, that is called on componentDidMount()
But in both cases the state is set in componentDidMount() and the final result returned of this.state looks like the screenshot attached.
Any help and / or improvements on this will be much appreciated.
You should never set the state values manually. You should just remove the line that sets the images in the state before calling setState. That line prevents the rendering since after that react can not detect any changes when you set the state using setState:
getImages = () => {
let firebase = loadFirebase()
var storageRef = firebase.storage().ref('chest')
storageRef.listAll().then((result) => {
let data = []
result.items.forEach((imageRef) => {
imageRef.getDownloadURL().then((url) => {
data.push(url);
this.setState({ images: data });
}).catch((error) => {
// Handle any errors
})
});
}).catch((error) => {
// Handle any errors
})
}

How to handle Promise in custom react useEffect

I have two components which i am working with. In the first component, i made a custom useEffect hook that retrieves data from my server. Please see the code below:
Code snippet One
import {useState, useCallback} from 'react';
import {stageQuizApi} from '../api/quiz';
import {QuestionService} from "../services/IdDbServices/question_service";
const usePostData = ({url, payload, config}) => {
const [res, setRes] = useState({data: null, error: null, isLoading: false});
const callAPI = useCallback(() => {
setRes(prevState => ({...prevState, isLoading: true}));
stageQuizApi.patch(url, payload, config).then( res => {
setRes({data: res.data, isLoading: false, error: null});
const questionInDb = {};
const {NoTimePerQuestion,anwser, question, playerDetails, option} = res.data.data;
const {playerid,anwserRatio, name} = playerDetails
questionInDb.timePerQuestion = NoTimePerQuestion;
questionInDb.anwserRatio = anwserRatio;
questionInDb.options = option;
questionInDb.answer = anwser;
questionInDb.playerId = playerid;
questionInDb.name = name;
questionInDb.question = question;
const Service = new QuestionService();
Service.addStudent(questionInDb).
then(response=>console.log(response))
.catch(err=>console.log(err));
}).catch((error) => {
console.log(error)
if (error.response) {
const errorJson = error.response.data
setRes({data: null, isLoading: false, error: errorJson.message});
} else if (error.request) {
setRes({data: null, isLoading: false, eror: error.request});
} else {
setRes({data: null, isLoading: false, error: error.message});
}
})
}, [url, config, payload])
return [res, callAPI];
}
export default usePostData;
The above module has two purpose. It first makes an axios request to my endpoint and secondly makes a database insertion to browser IndexDb (similar to localstorage but with sql approach) ( like inserting data into the database using the response that was gotten from the first request. so typically i have a promise in the outer .then block. This part:
Code snippet Two
const questionInDb = {};
const {NoTimePerQuestion,anwser, question, playerDetails, option} = res.data.data;
const {playerid,anwserRatio, name} = playerDetails
questionInDb.timePerQuestion = NoTimePerQuestion;
questionInDb.anwserRatio = anwserRatio;
questionInDb.options = option;
questionInDb.answer = anwser;
questionInDb.playerId = playerid;
questionInDb.name = name;
questionInDb.question = question;
const Service = new QuestionService();
Service.addStudent(questionInDb).
then(response=>console.log(response))
.catch(err=>console.log(err));
Here is the problem, I am trying to maintain state as i want the result of this module to be shared in another route and i don't want to hit the server again hence i inserted the result into indexDb browser storage. Here is the code that executes the above module:
Code snippet Three
const displaySingleQuestion = ()=>{
OnUserGetQuestion();
history.push('/player/question');
}
The above method is called from my first route /question and it is expected to redirect user to the /player/question when the displaySingleQuestion is called.
On the new route /player/question i then want to fetch the data from IndexDb and update the state of that component using the useEffect code below:
Code snippet Four
useEffect(()=>{
const getAllUserFromIndexDb = async()=>{
try{
const result = await new Promise((resolve, reject) => {
service.getStudents().then(res=>resolve(res)).catch(err=>reject(err))
});
console.log('it did not get to the point i was expecting',result)
if(result[0]){
console.log('it got to the point i was expecting')
const singleQuestion = result[0];
const questionPage = playerQuestionToDisplay;
questionPage.name = singleQuestion.name;
questionPage.anwserRatio = singleQuestion.anwserRatio;
questionPage.answer = singleQuestion.answer;
questionPage.options = singleQuestion.options;
questionPage.playerId = singleQuestion.playerId;
questionPage.question = singleQuestion.question;
questionPage.timePerQuestion = singleQuestion.timePerQuestion;
return setplayerQuestionToDisplay({playerQuestionToDisplay:questionPage})
}
}
catch(error){
console.log(error)
}
}
getAllUserFromIndexDb();
return function cleanup() {
setplayerQuestionToDisplay({playerQuestionToDisplay:{}})
}
},[history.location.pathname]);
The problem is that only one Button click (Code snippet three)(displaySingleQuestion()) triggers the whole functionality and redirect to the /player/question page but in this new route the state is not been set until a page reload as occurred, i tried debugging the problem and i found out that when the button is clicked i found out that Code snippet two is executed last hence when Code snippet Four ran it was in promise and until a page reloads occurs the state of the component is undefined
Thanks for reading, Please i would appreciate any help in resolving this issue.

Really need some help figuring out the logic of componentWillMount() prior to render

this might be kind of long read, I've read and tried so many solutions without success! Essentially what I have is three MySQL tables, one with a list of users, and one with a list of file data. They are paired with a third table, which has a column for user id and a column for file id.
When a user logs into the app, it grabs their ID from Table 1, goes to Table 3, finds all the file IDs that are in the same row as their user ID, and then returns the file information from Table 2. Mostly straight forward, except it's not.
My current code:
componentWillMount() {
this.getClientFiles();
}
Which calls:
getClientFiles() {
let id = this.props.currentUser.user_id;
let file_refs = [];
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.setState({
file_refs
});
this.getFileData();
});
}
My understanding of this is that this.getFileData(); should ONLY run once the axios GET request is successful (because of .then). The file refs are all returned, and the added to an array and put in state for the duration of the client's session.
Then this should run:
getFileData() {
let fileRefs = this.state.file_refs;
let fileData = [];
for (let i = 0; i < fileRefs.length; i++) {
axios
.get("/files/get-file/" + fileRefs[i])
.then(res => {
fileData.push(res.data.response);
this.setState({
client_files: fileData,
returned_data: true
});
})
.catch(err => console.log(err.response.data));
}
}
Here, the function cycles through the fileRefs in state, makes a call for each reference ID, and returns that to fileData and saves it to state.
The problem.... on first page load after a login, the files do not render. If you hit cmd+R to refresh, boom there they are. I understand the chain of promises, and the async nature of JS functions, I understand that componentWillMount() should run prior to the mounting of the component, and that setState should trigger a re-render of a component.
Things I've tried:
1) Adding the following code in after render() prior to return( :
if (this.state.returned_data === false) {
this.getClientFiles();
}
The result is a flickering of renders, 4-5 of them, as the functions run async before the state of returned_data is set to true.
2) Moving the setState({ returned_data: true }) into the getClientFiles() function. This just ends the render early, resulting in no files until the page is refreshed.
3) Swapping out componentWillMount() for componentDidMount().
Clearly, there is a fundamental aspect of the chain of functions and React's built in methods that I'm missing.
Can anybody help?
EDIT #1
The issue seems to be that on first render, let id = this.props.currentUser.user_id; is undefined, so the call in getClientFiles is actually going to /users/get-client-files/undefined
EDIT #2 - Requested by #devserkan
I hope this is what you wanted :)
First load
get-client-files/${id}: Returns an empty array
/get-file/" + fileRefs[i]: Doesn't run
Second load:
get-client-files/${id}: Returns array with 5 items
/get-file/" + fileRefs[i]: Runs appropriately 5 times with the details of each file.
So clearly, the issue is with the fact that get-client-files/${id} isn't getting anything because it doesn't have the ${id} to search from. The ID is passed down via props, but doesn't seem to be available immediately.
EDIT #3
Here is the function that gets the ID, and sets it to state.
getUser = () => {
let localToken = localStorage.getItem("iod_tkn");
axios({
url: "/admins/current",
method: "get",
headers: {
Authorization: localToken
}
})
.then(result => {
this.setState({
isLoggedIn: true,
user: result.data,
user_id: result.data.user_id
});
})
.catch(err => {
this.setState({ isLoggedIn: false });
console.log(err);
});
};
And App.js renders the following:
render() {
const { loading } = this.state;
if (loading) {
return <Spinner />;
}
return (
<AdminProvider>
<FileProvider>
<Provider>
<Appbar isLoggedIn={this.state.isLoggedIn} logout={this.logout} />
<Main
getUser={this.getUser}
isLoggedIn={this.state.isLoggedIn}
currentUser={this.state.user}
/>
<BottomNav />
</Provider>
</FileProvider>
</AdminProvider>
);
}
So with passing this.state.user into Main.js, that component should re-render once the props have been received, right?
Since your user_id is coming from an async job, you should do a conditional rendering. Like:
{ user_id && <ClientDashboard user_id={user_id} ... /> }
Also, you can clean up your code a little bit more maybe :) Here I am mimicking your app.
const userFiles = [
{ file_id: 1, client_name: "foo" },
{ file_id: 2, client_name: "bar" },
{ file_id: 3, client_name: "baz" },
];
const files = [
{ file_id: 1, name: "fizz", size: 10 },
{ file_id: 2, name: "buzz", size: 20 },
{ file_id: 3, name: "fuzz", size: 30 },
];
const fakeRequest = () => new Promise( resolve =>
setTimeout( () => resolve(userFiles), 1000)
);
const fakeRequest2 = id => new Promise(resolve => {
const file = files.find( el => id === el.file_id );
setTimeout(() => resolve(file), 1000)
}
);
class App extends React.Component {
state = {
file_refs: [],
client_files: [],
returned_data: false,
}
componentDidMount() {
this.getClientFiles();
}
getClientFiles() {
fakeRequest()
.then(res => {
const file_refs = res.map( el => el.file_id );
this.setState({
file_refs
});
this.getFileData();
});
}
getFileData() {
const {file_refs: fileRefs} = this.state;
const promiseArray = fileRefs.map( id => fakeRequest2( id ) );
Promise.all( promiseArray )
.then( results => this.setState({
client_files: results,
returned_data: true,
}))
}
render() {
const { file_refs, client_files } = this.state;
return (
<div>
{!!file_refs.length && <p>File_refs: {JSON.stringify(file_refs)}</p>}
{!!client_files.length && <p>Client files: {JSON.stringify(client_files)}</p>}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I don't like for loops :)
The problem is that in componentWillMount() an async call might not retrieve the results on time before the render of the mount phase happens, so you will have unexpected side effects. Most probably the component will render with empty data.
The best place to render data from an async call is componentDidMount().
As a side note, from 16.3 version on, componentWillMount() is considered an unsafe method of the lifecycle, and in future versions will be removed, so you better not use it anymore.
I think there's an issue with your code structuring. setState is an async function which takes a callback as a second parameter. You should take its advantage. You can execute a function after setState is finishing and utilize updated state using the second param callback (updater function) like:
this.setState({
file_refs
}, () => {
this.getFileData();
});
EDITED Second option you shouldn't setState file_refs unless you're using it in your render method.
Try this:
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.getFileData(file_refs);
});
getFileData(file_refs) {
let fileRefs = file_refs;
let fileData = [];
// rest of your code
}
Let me know if the issue still persists. Happy to help

flickering doing setState using nested api calls in react

I want to load a bunch of data that involves multiple APIs, I did setState in foreach, it worked but I think the design is wrong, as I see flickering on my screen.
API.fetchMain().then(main => {
main.forEach(o => {
const main_id = o.main_id
this.setState({
main: o
})
API.fetchSub(main_id)
.then(sub => {
this.setState({
sub
})
API.fetchOthers(main_id, sub.id)
.then(others => {
this.setState({
others
})
})
})
})
}
I think I should use promises to refactor, I tried but I think my design was wrong.
API.fetchMain().then(main => {
let promise = []
main.forEach(o => {
const main_id = o.main_id
this.setState({
main: o
})
promise.push(
API.fetchSub(main_id)
.then(sub => {
return API.fetchOthers(main_id, sub.id)
})
)
})
Promise.all(promise).then(resp => console.log('do setState here'))
}
Need help.
It looks to me that you are fetching a resource that provides you with information about how to make further requests. If you are open to using a fetch library I would recommend axios. Heres how I would envision it looking
import axios from 'axios'
fetch(){
// Make the initial request
var options = { url: "URL of main resource", method: "GET" }
axios(options).then(res => {
// Create an array of next requests from the response
var next_requests = res.data.main.map(id => axios.get(`${resource_url}/${id}`))
//Make the requests in parallel
axios.all(next_requests).then(axios.spread(() => {
//since we don't know how many request we can iterate
//over the arguments object and build the new state
var newState = {}
arguments.forEach(i => {
// how you want to structure the the state before setting it
})
this.setState(newState)
})
}).catch(err => //error handling logic here)
}
From my understanding of your question you could also (since you are using react) break your fetch request into components that get called when they mount. A quick example:
const class MainComp extends Component {
constructor(props){
super(props)
this.state = {
main: []
}
}
componentDidMount(){ this.fetchMain() }
fetchMain() {
axios.get('url').then(res =>
this.setState({main_id: res.data.main.id})
}
sendSubFetchToParent(dataFromChild){
// Do what you need to with the data from the SubComp child
}
render(){
return (
{this.state.main.map(id => <SubComp id={id) afterFetch={this.sendSubFetchToParent}/>}
)
}
}
const class SubComp extends Component {
componentDidMount(){ this.fetchSub() }
fetchSub() {
//Pass the results to the parent after the fetch completes.
// You can add the usual error handling here as well.
axios.get('url').then(res => this.props.afterFetch(res.data))
}
render(){
//Return null or render another sub component for further nested requests
return null
}
}
In the above, MainComp initiates a request. When it gets a response (which in your example is an array) we set that response to the state. This triggers rerender which will mount n number of SubComp. When those mount they will initiate their requests to get data. For SubComp we pass a callback from the parent so SubComp can send its fetch response back to MainComp (And handle it appropriately by setting state etc etc). You can return null in SubComp or have it mount a component that will make further request.
In that way your fetch requests are now componentized.

Categories

Resources