React Class passing stale props to Child stateless component - javascript

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

Related

React JS Updating item in State object to take effect immediately

React JS class component
I know there have been many posts on this subject but I can't seem to get this scenario to work.
Basically on my HandleClickSave event I want to update an item in my object in state without affecting the other values and then passing this updated oblect onto my service to get updated in the db.
The 'item' in question is the 'design' from the (unLayer) React-email-editor.
Problem is after the service is run in 'HandleClickSave' point 3 below, the receiving field 'DesignStructure' in the db is NULL every time. The other two fields are fine as these are saved to state object elsewhere.
Part of the problem is that the Email-Editor doesn't have an 'onChange' property which is where I would normally update the state. The other two values in the object are input texts and they do have an onChange which is how their state counterparts are updated.
This is the object 'NewsletterDesign':
{
"DesignId": "1",
"DesignName": "DesignLayout 1 Test",
"DesignStructure": null
}
In my React class component...
this.state = {
NewsletterDesign: {}
}
And the HandleClickSave event....
HandleClickSave () {
const { NewsletterDesign } = this.state
this.editor.saveDesign((design) => {
this.setState(prevState => ({
NewsletterDesign: {
...prevState.NewsletterDesign,
DesignStructure: design
}
}));
// Update to database passing in the object 'NewsletterDesign'. Field 'DesignStructure' in db is null every time, but other two fields are updated.
NewsletterService.UpdateCreateNewsletterDesign(NewsletterDesign)
etc....etc..
React's setState is not update immediately. read more here.
You can simply do it inside setState by
this.setState(prevState => {
const newState = {
NewsletterDesign: {
...prevState.NewsletterDesign,
DesignStructure: design
}
};
NewsletterService.UpdateCreateNewsletterDesign(newState.NewsletterDesign);
return newState;
});
The setState is an async operation. Meaning, that it's not guaranteed that the new state that you have updated will be accessible just after the state is updated. You can read more here
So in such cases, one of the way is to do the required operation first and then use the result at multiple places.
HandleClickSave () {
const { NewsletterDesign } = this.state
this.editor.saveDesign((design) => {
let newNewsletterDesign = { ...NewsletterDesign,
DesignStructure: design
};
this.setState(newNewsletterDesign);
NewsletterService.UpdateCreateNewsletterDesign(newNewsletterDesign)

Multiple set state within multiple api call inside componentDidMount in ReactJS

I am new in React and trying to call multiple api call within componentDidMount function.
My code is
componentDidMount() {
Promise.all([
axios.get(<url1>),
axios.get(<url2>)
]).then(([res1, res2]) => {
// call setState here
const users = res1.data.users;
this.setState({ users: users});
const banks = res2.data.banks;
this.setState({ banks: banks});
console.log("Users")
console.log(users) // It works
console.log("Banks")
console.log(banks) // It works
})
}
render() {
console.log(this.state.users.length) // Gives length
console.log(this.state.banks.length) // Gives undefined
return (
<div className='popup'></div>
)
}
The problem is inside render function the second state banks length is undefined.
How can I do multiple setstate inside componentDidMount.
Any help is highly appreciated.
Update: Resolved
The mistake was
constructor(props) {
super(props);
this.state = {
users: [],
//MISSING BANKS array
}
}
You should set state in a single update, updating both values at the same time. Otherwise you are instructing React to initiate two renders, the first would contain users value and an undefined value for banks (depending on your initial state declaration). This render would be quickly followed by a second pass, in which both state values users and banks would be defined.
The below example should work as required, in a single render.
Promise.all([
axios.get(<url1>),
axios.get(<url2>)
]).then(([res1, res2]) => {
// call setState here
const users = res1.data.users;
const banks = res2.data.banks;
this.setState({ users, banks });
});
On the other hand, if for some strange requirement you actually want two sequential renders you can use setState's done callback; example below.
this.setState({ users }, () => {
this.setState({ banks });
});
This will ensure the first render is complete before requesting a new render via setState.

React Event AJAX Call Race Condition

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.

Updating/ Pushing new data into NgRedux state

I am new to Angular and writing service which I will be using to add new addresses (posting to a REST API).
The saveAddress method call returns a newly created address object on server.
Which I wanted to push into the already existing array of addresses in store. I am trying to do something like:
saveAddress( payload ) {
this.httpClient.post( this.endpoint, payload).subscribe(
response => {
this.ngRedux.select( s => s.addresses ).subscribe( addresses => {
let data = addresses.data.push( response )
this.ngRedux.dispatch({ type: ADD_ADDRESS_SUCCESS, payload: data })
})
},
err => {
this.ngRedux.dispatch({ type: ADD_ADDRESS_ERROR })
}
)
}
How may I do it properly?
You must never alter the Store (state) anywhere else than in a reducer. The reducers receives an action (ADD_ADDRESS_SUCCESS) with its payload ({address: {...}}) and then updates the Store with that information:
reducer(state = initialState, action) {
switch (action.type) {
case ADD_ADDRESS_SUCCESS:
return Object.assign({}, state, {
addresses: state.addresses.push(action.payload.address)
})
default:
return state
}
}
Note, that we always make a copy of the state and do not mutate it.
To really understand it, please read the documentation for #angular-redux/store.
For API calls you should use Epics: Think of an Epic as pipeline that transforms an action into one or multiple other actions while handling a side effect. In your case the epic reacts to the ADD_ADDRESS_REQUEST, makes an API call with the payload and then transforms the action into either ADD_ADDRESS_SUCCESS or ADD_ADDRESS_ERROR, depending on the result of the API call. It will never update the state itself but delegate this to the reducer handling ADD_ADDRESS_SUCCESS and ADD_ADDRESS_ERROR respectively.
Read more about epics in the corresponding #angular-redux/store docs.

Redux async dispatch causes connected component error

When a button is clicked, then an API call is made (simulated by setTimeout) and when complete, that item is removed from the state. I would expect the component to not attempt to be rendered, but it is and fails, because the state it needs is no longer there.
The first fiddle showing the desired behaviour works because the call to dispatch is synchronous:
dispatch({
type: 'REMOVE',
id
});
The second fiddle shows the error because the call to dispatch is made asynchronously:
setTimeout(() => {
dispatch({
type: 'REMOVE',
id
});
}, 0);
I assumed the parent component would no longer try to render the child component because that item has been filtered out of the array. How do I remove the item from the state and prevent the component trying to re-render?
Yes this is an issue because by default redux doesn't support async calls like so what you are looking for it redux-thunk
https://github.com/gaearon/redux-thunk
Scroll down to motivation and it will basically have the exact same code you have posted here.
:::EDIT:::
Based off of your comment below I updated your code I think one of the main sources of the problem was connecting the Item component with map state to props.
const mapStateToProps = (state, { id }) => {
var {text} = state.items.filter((x) => x.id === id)[0];
return {
id: id,
text: text
};
};
const mapDispatchToProps = (dispatch) => {
return {
onRemove(id) {
setTimeout(() => {
dispatch({
type: 'REMOVE',
id
});
}, 0);
}
};
};
I know you probably didn't need it / or want it but I refactored the code a little bit to make it a little more clear but I might have gotten carried away and took it to far from your original example but if you want to take a look its posted here.
https://jsfiddle.net/kriscoulson/ogkyspsp/

Categories

Resources