React Event AJAX Call Race Condition - javascript

Basic Scenario
I have a React textbox controlled component whose onChange event eventually triggers an AJAX call to a server-side API. The results of this call may potentially change the value of the textbox. So, there is a call to setState in the AJAX call's callback.
Basic Problem
I am having trouble finding a way to smoothly, consistently update this value when changes are made to the input before the AJAX call completes. There are two approaches I have tried so far. The main difference is in how eventually the AJAX call happens.
Approach 1
My first attempt calls setState with the immediately entered data, which eventually triggers a re-render and componentDidUpdate. The latter then makes the AJAX call, on the condition that the state data in question is different.
handleChange(event) {
const inputTextValue = event.target.value;
setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}
componentDidUpdate(lastProps, lastState) {
const inputTextValue = this.state.inputText;
if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
$.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: inputTextValue })
// set other AJAX options
}).done((response) => {
setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
});
}
}
This approach has the advantage of quickly updating the state to reflect the user's immediate input. However, if two inputs are made quickly, a sequence such as the following occurs:
Event handler 1 fires with value '1'
Handler 1 calls setState with value '1'
Component re-rendered from change in state
componentDidUpdate triggered from re-render
Value '1' is different from last value, so
AJAX call 1 made with value '1'
While AJAX call 1 in progress, event 2 handler fires with value '12'
Handler 2 calls setState with value '12'
componentDidUpdate triggered from re-render
Value '12' is different from '1', so
AJAX call 2 made with value '12'
While AJAX call 2 in progress, AJAX call 1 returns with value '1'
AJAX callback 1 calls setState with value '1'
componentDidUpdate triggered from re-render
Value '1' is different from '12', so
AJAX call 3 made with value '1'
While AAJX call 3 in progress, AJAX call 2 returns with value '12'...
TL;DR an infinite loop occurs despite the last-state check in componentDidUpdate, since two overlapping AJAX calls give alternating values to setState.
Approach 2
To address this, my second approach simplifies the system and makes the AJAX call directly from the event handler:
handleChange(event) {
$.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: inputTextValue })
// set other AJAX options
}).done((response) => {
setState({ inputText: response.validatedInputTextValue });
});
}
If I do this, however, the immediate update of the controlled component value is stalled until the AJAX call completes and calls setState. It is simple and stable, only setting state and rendering once; but stalling input while waiting on an AJAX call is bad UX. The first approach at least has some semblance of an (overly) immediate update.
Approach 3?
While I am waiting for an answer, I am going to implement the following Approach 3, which is basically an enhanced version of Approach 1:
Add a request ID to the AJAX call data which is incremented every time the call is made
Echo the request ID back in the response
In the callback, if the current request ID is greater than that of the response, the response has expired data
If the response data is not expired, call setState
Question
I am still relatively new to React. I imagine someone else has encountered this use case, but I am having trouble finding a solution. I would like a way to set the state and update the component's value immediately, a la Approach 1, and still have Approach 2's data stability. Approach 3 seems promising, but a little too complicated. Is there an elegant pattern that accomplishes this?

The suggested solution (#1) has a big caveat:
You have no guarantee that the first request will return before the second.
In order to avoid it, you can follow one of these approaches:
Lock the select input:
Your select component:
const Select = props => {
const {disabled, options} = props;
return (<select disabled={disabled}>
{ options.map(item => <option value={item}> {item} </option> }
</select>)
}
Your logical component:
class LogicalComponent extends React.Component {
constructor(props) {
this.state = {
selectDisabled: false;
options: ['item1', 'item2', 'item3'],
inputText: ''
}
}
handleChange(event) {
const inputTextValue = event.target.value;
setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}
componentDidUpdate(lastProps, lastState) {
const inputTextValue = this.state.inputText;
if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
// disabling the select until the request finishes
this.setState({ selectDisabled: true });
$.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: inputTextValue })
// set other AJAX options
}).done((response) => {
//re-enabling it when done
setState({ inputText: response.validatedInputTextValue, selectDisabled: false }); // will also trigger componentDidUpdate
// don't forget to enable it when the request is failed
}).fail(res => this.setState({selectDisabled: false}));
}
}
render() {
const { selectDisabled, options, inputText } = this.state;
return <>
<Select disabled={selectDisabled} options={options} />
<input type="text" value={inputText}/>
<>
}
}
Cancel the request that's in progress
If you already have an AJAX request in progress, you can cancel it and fire a new one. This will guarantee that only the recent request is returned.
class LogicalComponent extends React.Component {
constructor(props) {
this.requestInProgress = null;
this.state = {
options: ['item1', 'item2', 'item3'],
inputText: ''
}
}
handleChange(event) {
const inputTextValue = event.target.value;
setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}
componentDidUpdate(lastProps, lastState) {
const inputTextValue = this.state.inputText;
if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
// checking to see if there's a request in progress
if(this.requestInProgress && this.requestInProgress.state() !== "rejected") {
// aborting the request in progress
this.requestInProgress.abort();
}
// setting the current requestInProgress
this.requestInProgress = $.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: inputTextValue })
// set other AJAX options
}).done((response) => {
setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
// don't forget to enable it when the request is failed
})
}
}
render() {
const { selectDisabled, options, inputText } = this.state;
return <>
<Select disabled={selectDisabled} options={options} />
<input type="text" value={inputText}/>
<>
}
}

I ended up reverting back to Approach 1, but debouncing the input to eliminate the overlap.
In particular, I used Lodash to debounce a method refactored from the code in componentDidUpdate that actually made the AJAX call:
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.validateInput = this.validateInput.bind(this);
this.validateInputDebounced = _.debounce(this.validateInput, 100);
}
handleChange(event) {
const inputTextValue = event.target.value;
setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}
componentDidUpdate(lastProps, lastState) {
const inputTextValue = this.state.inputText;
if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
validateInputDebounced(inputTextValue);
}
}
validateInput(newInputTextValue) {
$.ajax({
url: serviceUrl,
data: JSON.stringify({ inputText: newInputTextValue })
// set other AJAX options
}).done((response) => {
setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
});
}
This is in part based on the work done here: https://medium.com/#platonish/debouncing-functions-in-react-components-d42f5b00c9f5
Edit
Upon further examination, this method falls short as well. If the AJAX call is sufficiently longer than the debounce, the requests potentially resolve out of order again. I think I will keep the debounce logic to save on network traffic; but the accepted solution, cancelling a previous in-progress request, sufficiently addresses the issue.

Related

React Class passing stale props to Child stateless component

My Parent Component represents a form.
The users filling in the form have access to information in the form that is updated in real time as they update certain fields.
The Issue I am running into is. On one of these updates when we fetch the new data and pass it to the child randomly sometimes the child is receiving stale props. From the previous request.
The structure is something like this.
export class Form extends React.Component<Props, State> {
fetchUpdates = async (payload) => {
this.setState({ isLoadingUpdates: true })
await Service.getUpdates(payload)
.then(response => {
this.setState({ isLoadingUpdates: false, updates: response.data })
})
.catch(({ data: errors }) => this.setState({ isLoadingUpdates: false }))
}
}
render () {
const {
updates,
isLoadingUpdates,
} = this.state
<FormCombobox
onChange={this.fetchUpdates}
md={10}
name="field"
id="field"
label="Field"
onMenuOpen={() => forceCheck()}
openMenuOnClick
selectRef={this.itemSelect}
value={values.item}
options={itemOptions || []}
/>
<Info
data={updates}
errorMessage={this.state.updatesError}
/>
}
}
It doesn't occur every time but randomly either when the form is first updated or on one of the following updates the < Info > container recieves the previous requests response data. How can I stop the parent from passing stale data?
The problem here is that when fetchUpdates is called multiple times it gets out of order due to network delay. Let's say fetchUpdates is called three times, and let's say the request takes 5, 2 and 4 seconds respectively to complete. In this case, you can see that the second request calls setState before the first request. As a result, the info component gets passed the first value after the second value. This is the reason why it is intermittent.
Using await here won't help, because the fetchUpdates function calls are independent of each other.
One more thing, I noticed that you have isLoadingUpdates. But it's not being used anywhere in the code. And also doing,
if (!this.state. isLoadingUpdates) {
await Service.getUpdates(payload)
.then(response => {
this.setState({ isLoadingUpdates: false, updates: response.data })
})
.catch(({ data: errors }) => this.setState({ isLoadingUpdates: false }))
}
won't work because then it means you will miss keypresses when the network call is ongoing.
I would suggest using a debounce for the inputs. You can find how to do debounce here: Perform debounce in React.js

Ternary the operator inside the button and call the function

Is it possible to set the ternary operator and call the start() function. Under the influence of data from API, pending will change totrue.
I am trying to use the ternary operator inside the button and initiate the click event. Pending = true call the click event, thestart ()function.
{this.state.pending ? this.start() : null}
class Button extends Component {
constructor() {
super();
this.state = {
isRunning: false,
pending: false
};
}
componentDidMount() {
axios.get
axios({
url: "https://app/api/v1/running",
method: "GET",
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
console.log(response);
this.setState({
pending: response.data.pending //true
});
})
.catch(error => {
console.log(error);
})
}
start = () => {
console.log('AAAA');
}
render () {
return (
<button {this.state.pending ? this.start() : null} onClick={this.start}>
Start
</button>
);
}
}
You could take one of two approaches here.
1. Run the function straight from the api response success callback
.then(response => {
console.log(response);
this.setState({
pending: response.data.pending //true
});
this.start(); //Here
})
If you want to update the state before calling that function in any case then simply call the function in the callback of setStates second parameter. setState also accepts a function as its parameter and executes that function after performing the update. This is important to remember because setState operates asynchronously. So, If you want to make sure before your call the value has to update for some reason you can do this way. More about it here: https://reactjs.org/docs/react-component.html#setstate
.then(response => {
console.log(response);
this.setState({
pending: response.data.pending //true
}, this.start);
})
2. Use a component lifecycle method to listen for state changes and run the function
componentDidUpdate = (prevProps, prevState) => {
if(prevState.pending === false && this.state.pending === true){
this.start()
}
}
Every time you have a state or prop change componentDidUpdate executes. Note that it does not update when the component first mounts. And from the look of your code since pending is local to this component only you don't need to capture the case when it mounts firsts. More reference on lifecycle methods: https://reactjs.org/docs/react-component.html
Hope that clears your confusion here. Cheers!

Is it possible that this.state has not been updated yet in a handleClick function?

React Doc says
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state
Does this mean that i can't trust this.state at any place?
For example:
MyComponent extends Component {
// ...
handleClick () {
// ...
fetch(targetUrl, {
method: 'POST',
body: JSON.stringify({
param1: this.state.param1
})
})
}
// ...
}
Does it mean that i may send wrong param1 to targetUrl(Since this.state may not been updated yet)?
set state is asyncchronus . if you want to do something with state like
this.setState({param1:true},()=>{
fetch(targetUrl, {
method: 'POST',
body: JSON.stringify({
param1: this.state.param1
})
})
})
it takes a call back where you can get state after state is updated and perform action with the updated one.. hope it helps :)
It depends on what you do when handleClick.
If your handleClick method is like this:
handleClick () {
this.setState({ param1: 'something'});
fetch(targetUrl, {
method: 'POST',
body: JSON.stringify({
param1: this.state.param1
})
})
}
Then when you call fetch, the this.state.param1 is not updated yet. because setState is asynchronous.
And if you don't setState in handleClick, then you will be fine with your code.
Read more about setState Beware: React setState is asynchronous!
It means this.setState({}) updates state asynchronously and not instantly. Reaat will decide when to update the state via this.setState({}).
So if you do this.setState({data: response}), it is not guaranteed that it will state instantly, it might take some time to update it.
You can check this using below code:
this.setState({data: response}, () => console.log('state updated'));
Above is updating state and when updated it will execute call back method.

React (16.4.1) setState() updates state one onChange event behind the current onChange event with react-select component

I have 2 react-select components and the behavior I wish to set is that when the user makes their selection from both select components (order does not matter) then an ajax will be triggered to pull the data from the server. The selection from both of these components is required to fully populate the GET parameters for the ajax call.
I have these 2 handlers for the onChange event on the react-select elements:
filterSiteSelect(selection) {
const siteId = selection.id;
const siteName = selection.name;
this.setState({
siteId, siteName
}, this.getTableData());
}
filterLineTypeSelect(selection) {
const lineTypeId = selection.id;
const lineTypeName = selection.name;
this.setState({
lineTypeId, lineTypeName
}, this.getTableData());
}
And my getTableData() method looks like:
getTableData() {
const {
productId, siteId, lineTypeId, stages, tableUrl
} = this.state;
// tableUrl = `p=field&t=view&gapi=1&product_id=${productId}&site_id=${siteId}&line_type_id=${lineTypeId}&stage_ids=${stages}`
if (productId && siteId && lineTypeId && !_.isEmpty(stages)) {
Axios.get(tableUrl)
.then((result) => {
this.setState({
rawData: { ...result.data.data }
});
});
}
}
The behavior I am experiencing is that when the user select's an option from the second select box the ajax call does not fire. The user needs to go back and select something else to get the ajax call to fire and then it uses the first selection they chose.
I also tried to use ComponentDidUpdate() for the ajax call with this code (I removed the getTable() data from each of the setState() calls when I changed to componentDidUpdate(prevState)):
componentDidUpdate(prevState) {
const {
siteId, lineTypeId, stages
} = this.state;
if (lineTypeId !== prevState.lineTypeId && siteId !== prevState.siteId && !_.isEqual(stages, prevState.stages)) {
this.getTableData();
}
}
But what happens when using the componentDidUpdate() lifecycle method it fires the ajax over and over never stoping and I believe that is because the setState() is never updating the state for the last select component the user interacted with.
So I think I'm doing something wrong in my use/understanding of the setState() method (or the issue lies in the react-select component...).
Any insight, assistance, or discussion of what I'm trying to accomplish would be greatly appreciated!
When you pass a second argument to setState(), e.g. setState({foo: bar}, <something>), the <something> is supposed to be a callback function that gets called when setState() has finished updating. In your code, instead of passing your function this.getTableData as an argument, you are passing the expression returned from calling this.getTableData(), in this case undefined.
In your code here:
filterSiteSelect(selection) {
const siteId = selection.id;
const siteName = selection.name;
this.setState({
siteId, siteName
}, this.getTableData());
}
filterLineTypeSelect(selection) {
const lineTypeId = selection.id;
const lineTypeName = selection.name;
this.setState({
lineTypeId, lineTypeName
}, this.getTableData());
}
When you synchronously call setState() and add a state update to the queue, you are also synchronously calling this.getTableData() which runs immediately, checks some booleans in your state variables, maybe tries to do an ajax call, etc.
Try simply removing the () so that you're passing the function directly into setState as an argument instead of accidentally calling the function. :)
filterSiteSelect(selection) {
const siteId = selection.id;
const siteName = selection.name;
this.setState({
siteId, siteName
}, this.getTableData);
}
filterLineTypeSelect(selection) {
const lineTypeId = selection.id;
const lineTypeName = selection.name;
this.setState({
lineTypeId, lineTypeName
}, this.getTableData);
}
setState's second parameter is a callback. That means you should be passing a reference to the function to call, instead of calling the function itself.
this.setState({
siteId, siteName
}, this.getTableData());
Should be
this.setState({
siteId, siteName
}, this.getTableData);

Double rendering in React with asynchronous call in componentDidMount causing error

I'm building a blog application that has an articles index page, and from there you can click on an article to see the article or edit the article.
If you're going from the index page to the edit page, it works just fine because I already have all the articles in state. But if I refresh after I've gone to the edit-article page, I no longer have all the articles in state.
This is a problem because I'm making an asynchronous recieveSingleArticle call in the componentDidMount of my edit-article page, then I setState so my form is prepopulated. There's a double render which causes an "Uncaught TypeError: Cannot read property 'title' of undefined" error, presumably during the first render before the article has been received into state.
class ArticleEdit extends React.Component {
constructor(props) {
super(props);
this.state = {title: "", body: "", imageFile: ""};
this.handleChange = this.handleChange.bind(this);
this.handlePublish = this.handlePublish.bind(this);
this.handleFile = this.handleFile.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
componentDidMount() {
const { article, requestSingleArticle } = this.props;
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}
...
I tried wrapping my async calls inside of an "if (this.props.article)" but that didn't work. Is there a best way of dealing with this type of problem? Any advice greatly appreciated!
UPDATE:
Another solution that works is to have a componentDidUpdate in addition to componentDidMount. check in componentDidMount if this.props.article exists and if so, setState. And in componentDidUpdate, wrap the setState in the following conditional:
if (!prevProps.article && this.props.article)
Just check if the article is present in the props before calling async action
componentDidMount() {
const { article, requestSingleArticle } = this.props;
if (!(article && requestSingleArticle)) return; // this line
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}
Since you are not getting any render from this method , it means that the props are not yet obtained in the life cycle method componnetDidMount. So instead you can use componentWillReceiveProps like this
componentWillReceiveProps(nextProp) {
// this line here will check the article props' status so that
// we will not use setState each time we get a prop
if (this.props.article === nextProp.article) return;
// rest is just the same code from above
const { article, requestSingleArticle } = nextProp;
if (!(article && requestSingleArticle)) return; // this line
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}

Categories

Resources