Getting React to re-render after Promise is completed - javascript

I've simplified much of the code as the issue lies in the Promise and async/await part.
I wish for this component to get data from my API and create a plot from it. If the API is still retrieving, I want it to show the loading icon.
class Data extends Component {
state = {};
componentDidMount() {
this.setState({ data: this.getData() });
}
async getData() {
try {
const response = await axios.get('/api/data');
return response.data;
} catch (err) {
return [];
}
}
renderLoading() {
return <Loading/>; // this represents a loading icon
}
renderPlot(data) {
return <Plot data={data}/>; // this represents the plot that needs the data
}
render() {
return {this.state.data
? this.renderLoading()
: this.renderPlot(this.state.data)};
}
}
At the moment, what this does is check this.state.data, sees that it's undefined, and simply display the loading icon forever without ever checking it again. How can I make it recheck once the promise is completed? One issue to note is that renderPlot requires data to be finished, if I call renderPlot while the Promise is still pending, it doesn't process it properly.

Instead of calling setState before the data is ready, call this.getData() in componentDidMount and then call setState once the response data is ready. React will automatically re-render the component with state changes.
class Data extends Component {
state = {}
componentDidMount() {
this.getData()
}
getData() {
axios
.get('/api/data')
.then(({ data }) => {
this.setState({ data })
})
.catch(err => {
this.setState({ error: err.message })
})
}
render() {
return this.state.data
? <Plot data={this.state.data} />
: this.state.error
? <div>{this.state.error}</div>
: <Loading />
}
}

You need to call setState after your async operation completes. Try this in your componentDidMount()
componentDidMount() {
// Wait for the operation to complete before setting data
this.getData().then((d) => {
this.setState({ data: d});
});
}
Your code just sets data to be the Promise of the async operation.

Related

Waiting for async function in React component & Showing Spinner

Beginner here.
Trying to fetch some data from a server and display it in my react component once its fetched.
However, I am having trouble integrating the async function into my react component.
import React, { useState } from "react";
import { request } from "graphql-request";
async function fetchData() {
const endpoint = "https://localhost:3090/graphql"
const query = `
query getItems($id: ID) {
item(id: $id) {
title
}
}
`;
const variables = {
id: "123123123"
};
const data = await request(endpoint, query, variables);
// console.log(JSON.stringify(data, undefined, 2));
return data;
}
const TestingGraphQL = () => {
const data = fetchData().catch((error) => console.error(error));
return (
<div>
{data.item.title}
</div>
);
};
export default TestingGraphQL;
I'd like to simply show a spinner or something while waiting, but I tried this & it seems because a promise is returned I cannot do this.
Here you would need to use the useEffect hook to call the API.
The data returned from the API, I am storing here in a state, as well as a loading state to indicate when the call is being made.
Follow along the comments added in between the code below -
CODE
import React, { useState, useEffect } from "react"; // importing useEffect here
import Layout from "#layouts/default";
import ContentContainer from "#components/ContentContainer";
import { request } from "graphql-request";
async function fetchData() {
const endpoint = "https://localhost:3090/graphql"
const query = `
query getItems($id: ID) {
item(id: $id) {
title
}
}
`;
const variables = {
id: "123123123"
};
const data = await request(endpoint, query, variables);
// console.log(JSON.stringify(data, undefined, 2));
return data;
}
const TestingGraphQL = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
// useEffect with an empty dependency array works the same way as componentDidMount
useEffect(async () => {
try {
// set loading to true before calling API
setLoading(true);
const data = await fetchData();
setData(data);
// switch loading to false after fetch is complete
setLoading(false);
} catch (error) {
// add error handling here
setLoading(false);
console.log(error);
}
}, []);
// return a Spinner when loading is true
if(loading) return (
<span>Loading</span>
);
// data will be null when fetch call fails
if (!data) return (
<span>Data not available</span>
);
// when data is available, title is shown
return (
<Layout>
{data.item.title}
</Layout>
);
};
since fetchData() returns a promise you need to handle it in TestingGraphQL. I recommend onComponentMount do your data call. Setting the data retrieved into the state var, for react to keep track of and re-rendering when your data call is finished.
I added a loading state var. If loading is true, then it shows 'loading' otherwise it shows the data. You can go about changing those to components later to suit your needs.
See the example below, switched from hooks to a class, but you should be able to make it work! :)
class TestingGraphQL extends Component {
constructor() {
super();
this.state = { data: {}, loading: true};
}
//when the component is added to the screen. fetch data
componentDidMount() {
fetchData()
.then(json => { this.setState({ data: json, loading: false }) })
.catch(error => console.error(error));
}
render() {
return (
{this.state.loading ? <div>Loading Spinner here</div> : <div>{this.state.data.item.title}</div>}
);
}
};

setState Does Not Define State Variable (ReactJS)

I am a noob of ReactJS and I created a Homepage for Users to view after logging in or registering . I am getting the data from my custom API(NodeJS) but once I fetch the data and attempt to setState it appears that it is not updating the state variables.
Here is my code.
export class SideMenu extends Component {
constructor(props) {
super(props)
this.state = {
user: {},
};
}
componentDidMount() {
this.fetchData();
console.log(this.user);
}
fetchData = () => {
axios.get("http://localhost:3001/returningusers").then((response) => {
const data = response.data.User[0]
console.log(data)
this.setState({
user: data
})
})
}
I really need help with this, it has slowed down my momentum and I have no idea how to fix it. Thanks.
I'm not so sure that's the correct syntax for an ES6 class method. How about trying the below instead of the arrow function:
fetchData() {
axios.get("http://localhost:3001/returningusers").then((response) => {
const data = response.data.User[0]
console.log(data)
this.setState({
user: data
})
})
}
Also you need to return Promises and either await or then them. For example:
async componentDidMount() {
await this.fetchData(); // you need to wait until this finishes
console.log(this.state.user);
}
// and you need to return the Promise here
fetchData() => {
return axios.get("http://localhost:3001/returningusers").then((response) => {
const data = response.data.User[0]
console.log(data)
this.setState({
user: data
})
})
}

How to call a function that uses the state (which is set by another function) and updates state

I have two functions one that fetches data from an api and updates state according to that data, and a function that itterates over the data in the state and updates the state with the new data.
My problem is that i cant update the state in the second function. And i dont know where i have to call this function in order for it to be called after the first function and to use the data thats in the state.
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
});
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
addPhotos() {
const newCases = this.state.cases.map(({ photo_id, ...rest }) => {
const obj = { ...rest };
this.state.photos.find(data => {
if (data.id === photo_id) {
obj.file = data.file;
return true;
}
});
return obj;
});
console.log(this.state.cases);
this.setState({
'cases' : newCases
});
}
renderCases() {
this.addPhotos();
}
componentWillMount() {
this.getCases();
}
render() {
return (
<div>
{this.renderCases()}
</div>
)
}
}
This is what i now have
Where should i call the addPhotos function so it can update the state and still use the existing state data from the getCases function?
Thanks in advance!
So, first thing's first. The lifecycle method componentWillMount() is soon to be deprecated and is considered unsafe to use. You should be using componentDidMount().
As far as using the updated state in your addPhotos function, you can pass setState a callback function. A seemingly simple solution would be to just pass the addPhotos function as a callback into the setState being called in your getCases function.
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
}, this.addPhotos);
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
Another solution would be to call addPhotos() from componentDidUpdate instead.
Hope this helps!
Edit: Just some additional background information from the React docs.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Added some refactoring to your code, should work ok now, read comments for details
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(this.addPhotos) // don't need to set state, pass data to consumer function
.catch(console.error); // catch always gives error, don't need to check with if statement
}
addPhotos(response) {
const cases = response.data.cases // extract values
const photos = response.data.photos // extract values
// your .map iterator has O^2 complexity (run each item of cases on each item of photos)
// this new .map iterator has O complexity (run each item of cases)
const newCases = cases.map(({ photo_id, ...rest }) => {
const obj = {...rest};
const data = photos.find(item => item.id === photo_id);
if (data) {
obj.file = data.file
}
return obj;
});
this.setState({
cases: newCases,
photos
});
}
componentDidMount() { // better use didMount
this.getCases();
}
render() {
return (<div />)
}
}

React - How to pass returned data from an exported function to a component?

How can I pass data I receive from a get request pass over to a component? Whatever I tried wouldn't work but my thinking was as the code below shows..
Thanks!
export function data() {
axios.get('www.example.de')
.then(function(res) {
return res.data
})
.then(function(data) {
this.setState({
list: data
})
})
}
import {data} from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ""
};
}
componentWillMount() {
data();
}
render() {
return <p > this.state.list < /p>
}
}
You call this.setState inside of data()->then callback, so this is context of the then callback function. Instead you should use arrow functions (it does not have its own context) and pass component's this to data function using call
export function data() {
axios.get('www.example.de')
.then(res => res.data)
.then(data => {
this.setState({
list: data
})
})
}
import {data} from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ""
};
}
componentWillMount() {
data.call(this);
}
render() {
return <p > this.state.list < /p>
}
}
However, your data services must not know about setState and, event more, expect passing this from react component. Your data service must be responsible for retrieving data from server, but not for changing component state, see Single responsibility principle. Also, your data service can be called from another data service. So your data service should return promise instead, that can be used by component for calling setState.
export function data() {
return axios.get('www.example.de')
.then(res => res.data)
}
and then
componentWillMount() {
data().then(data=>{
this.setState({
list: data
})
});
}
your api shouldn't know anything about your component, you can easily do this with callback, like so -
export function data(callback) {
axios.get('www.example.de')
.then(res => callback({ data: res.data }))
.catch(err => callback({ error: err }));
}
By doing this you can easily unit test your api
So in your Test component, you simply do -
componentWillMount() {
data(result => {
const { data, error } = result;
if (error) {
// Handle error
return;
}
if (data) {
this.setState({ list: data });
}
});
}
Your request is a promise so you can simply return that from the imported function and use the eventual returned result of that within the component. You only want to be changing the state of the component from within the component.
export function getData(endpoint) {
return axios.get(endpoint);
}
Note I've changed the name of the function to something more "actiony".
import { getData } from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
// Your state is going to be an array of things, so
// initialise it with an array to spare confusion
this.state = { list: [] };
}
// I use ComponentDidMount for fetch requests
// https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/
componentDidMount() {
// We've returned a promise from `getData` so we can still use the
// promise API to extract the JSON, and store the parsed object as the
// component state
getData('www.example.de')
.then(res => res.data)
.then(list => this.setState({ list }))
}
}
Your external function doesn't have the correct context of this, so you'll need to call it with the correct context from within the component:
componentWillMount() {
data.call(this);
}
However, inside the API call, it still won't have the correct this context, so you can set a variable to point to this inside the data() function:
export function data() {
let that = this;
axios('http://www.url.com')
.then(function(res) {
return res.data
})
.then(function(data) {
that.setState({
list: data
})
})
}
Details of the this keyword
However, it's generally considered better practice to only handle your state manipulation from with the component itself, but this will involve handling the asynchronous nature of the GET request, perhaps by passing in a callback to the data() function.
EDIT: Updated with asynchronous code
//api.js
data(callback){
axios.get('www.url.com')
.then(res => callback(res));
}
//component.jsx
componentWillMount(){
data(res => this.setState({list: res}));
}

React - setState() on unmounted component

In my react component im trying to implement a simple spinner while an ajax request is in progress - im using state to store the loading status.
For some reason this piece of code below in my React component throws this error
Can only update a mounted or mounting component. This usually means
you called setState() on an unmounted component. This is a no-op.
Please check the code for the undefined component.
If I get rid of the first setState call the error goes away.
constructor(props) {
super(props);
this.loadSearches = this.loadSearches.bind(this);
this.state = {
loading: false
}
}
loadSearches() {
this.setState({
loading: true,
searches: []
});
console.log('Loading Searches..');
$.ajax({
url: this.props.source + '?projectId=' + this.props.projectId,
dataType: 'json',
crossDomain: true,
success: function(data) {
this.setState({
loading: false
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
this.setState({
loading: false
});
}.bind(this)
});
}
componentDidMount() {
setInterval(this.loadSearches, this.props.pollInterval);
}
render() {
let searches = this.state.searches || [];
return (<div>
<Table striped bordered condensed hover>
<thead>
<tr>
<th>Name</th>
<th>Submit Date</th>
<th>Dataset & Datatype</th>
<th>Results</th>
<th>Last Downloaded</th>
</tr>
</thead>
{
searches.map(function(search) {
let createdDate = moment(search.createdDate, 'X').format("YYYY-MM-DD");
let downloadedDate = moment(search.downloadedDate, 'X').format("YYYY-MM-DD");
let records = 0;
let status = search.status ? search.status.toLowerCase() : ''
return (
<tbody key={search.id}>
<tr>
<td>{search.name}</td>
<td>{createdDate}</td>
<td>{search.dataset}</td>
<td>{records}</td>
<td>{downloadedDate}</td>
</tr>
</tbody>
);
}
</Table >
</div>
);
}
The question is why am I getting this error when the component should already be mounted (as its being called from componentDidMount) I thought it was safe to set state once the component is mounted ?
Without seeing the render function is a bit tough. Although can already spot something you should do, every time you use an interval you got to clear it on unmount. So:
componentDidMount() {
this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}
componentWillUnmount () {
this.loadInterval && clearInterval(this.loadInterval);
this.loadInterval = false;
}
Since those success and error callbacks might still get called after unmount, you can use the interval variable to check if it's mounted.
this.loadInterval && this.setState({
loading: false
});
Hope this helps, provide the render function if this doesn't do the job.
Cheers
The question is why am I getting this error when the component should already be mounted (as its being called from componentDidMount) I thought it was safe to set state once the component is mounted ?
It is not called from componentDidMount. Your componentDidMount spawns a callback function that will be executed in the stack of the timer handler, not in the stack of componentDidMount. Apparently, by the time your callback (this.loadSearches) gets executed the component has unmounted.
So the accepted answer will protect you. If you are using some other asynchronous API that doesn't allow you to cancel asynchronous functions (already submitted to some handler) you could do the following:
if (this.isMounted())
this.setState(...
This will get rid of the error message you report in all cases though it does feel like sweeping stuff under the rug, particularly if your API provides a cancel capability (as setInterval does with clearInterval).
To whom needs another option, the ref attribute's callback method can be a workaround. The parameter of handleRef is the reference to div DOM element.
For detailed information about refs and DOM: https://facebook.github.io/react/docs/refs-and-the-dom.html
handleRef = (divElement) => {
if(divElement){
//set state here
}
}
render(){
return (
<div ref={this.handleRef}>
</div>
)
}
class myClass extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
this._isMounted = true;
this._getData();
}
componentWillUnmount() {
this._isMounted = false;
}
_getData() {
axios.get('https://example.com')
.then(data => {
if (this._isMounted) {
this.setState({ data })
}
});
}
render() {
...
}
}
Share a solution enabled by react hooks.
React.useEffect(() => {
let isSubscribed = true
callApi(...)
.catch(err => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed, ...err }))
.then(res => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed }))
.catch(({ isSubscribed, ...err }) => console.error('request cancelled:', !isSubscribed))
return () => (isSubscribed = false)
}, [])
the same solution can be extended to whenever you want to cancel previous requests on fetch id changes, otherwise there would be race conditions among multiple in-flight requests (this.setState called out of order).
React.useEffect(() => {
let isCancelled = false
callApi(id).then(...).catch(...) // similar to above
return () => (isCancelled = true)
}, [id])
this works thanks to closures in javascript.
In general, the idea above was close to the makeCancelable approach recommended by the react doc, which clearly states
isMounted is an Antipattern
Credit
https://juliangaramendy.dev/use-promise-subscription/
For posterity,
This error, in our case, was related to Reflux, callbacks, redirects and setState. We sent a setState to an onDone callback, but we also sent a redirect to the onSuccess callback. In the case of success, our onSuccess callback executes before the onDone. This causes a redirect before the attempted setState. Thus the error, setState on an unmounted component.
Reflux store action:
generateWorkflow: function(
workflowTemplate,
trackingNumber,
done,
onSuccess,
onFail)
{...
Call before fix:
Actions.generateWorkflow(
values.workflowTemplate,
values.number,
this.setLoading.bind(this, false),
this.successRedirect
);
Call after fix:
Actions.generateWorkflow(
values.workflowTemplate,
values.number,
null,
this.successRedirect,
this.setLoading.bind(this, false)
);
More
In some cases, since React's isMounted is "deprecated/anti-pattern", we've adopted the use of a _mounted variable and monitoring it ourselves.
Just for reference. Using CPromise with decorators you can do the following tricks:
(Live demo here)
export class TestComponent extends React.Component {
state = {};
#canceled(function (err) {
console.warn(`Canceled: ${err}`);
if (err.code !== E_REASON_DISPOSED) {
this.setState({ text: err + "" });
}
})
#listen
#async
*componentDidMount() {
console.log("mounted");
const json = yield this.fetchJSON(
"https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
);
this.setState({ text: JSON.stringify(json) });
}
#timeout(5000)
#async
*fetchJSON(url) {
const response = yield cpFetch(url); // cancellable request
return yield response.json();
}
render() {
return (
<div>
AsyncComponent: <span>{this.state.text || "fetching..."}</span>
</div>
);
}
#cancel(E_REASON_DISPOSED)
componentWillUnmount() {
console.log("unmounted");
}
}

Categories