I maintain an object array in my Context that keeps track of validation errors in my input form. Here's an example of it populated with one object:
validationErrors = [{id: 0, message: 'Already exists', name: 'vin'}]
The message is displayed underneath the input element in question, prompting the user to enter a new value.
When the user does so, an async call is made to check if the new value already exists in the DB. Before that call is made, a call is dispatched to this reducer:
case CLEAR_VIN_INFO: {
return {
...state,
validationErrors: [...state.validationErrors.filter(
validationError => validationError.name !== 'vin' || validationError.id !== action.id)],
vehicles: state.vehicles.map((vehicle: Vehicle) => {
if (vehicle.id === action.id) {
return {
...vehicle,
makeModelYear: ''
};
} else {
return vehicle;
}
})
};
}
It's doing a few things but the main thing I want to focus on is that this specific validation error, with id: 0 and name: vin are removed from validationErrors. Watching what's happening in Chrome DevTools, I can indeed confirm that it is being removed. In other words, validationErrors is set to an empty array.
But upon returning from the async call, the following code produces unexpected results:
console.log(validationErrors.find(error => error && error.name === 'vin' && error.id === vehicle.id));
It reveals the following: {id: 0, message: 'Already exists', name: 'vin'}
How is this possible?
By the way, checking validationErrors in Chrome DevTools again shows that it's still empty!
My intuition is telling me that this bug is due to a previous version of validationErrors being referenced instead. I've never encountered this before though, so am not sure what to do.
Any ideas?
Update: As a sanity check of sorts, I made a call to this reducer instead:
case CLEAR_ERRORS: {
return {
...state,
validationErrors: []
};
}
This one absolutely clears validationErrors before the async call is made. Yet, upon returning from the async call, validationErrors is still populated! Now I'm completely baffled.
Try to fetch the state and access validationErrors inside your async call, then it should be good... but need more explanation in your question.
Related
I'm having an issue with Typescript and it's giving me the following error listed below. The part of const formRef = useRef<HTMLFormElement>(null); seems to be good, but the issue looks to be with formRef.current.checkValidity().
How can I add the Typescript typing/get rid of the error?
Error:
(property) React.RefObject<HTMLFormElement>.current: HTMLFormElement | null
Object is possibly 'null'.ts(2531)
Code:
// React Hooks: Refs
const formRef = useRef<HTMLFormElement>(null);
// Send Verification Code
const sendVerificationCode = (event: any) => {
// Event: Cancels Event (Stops HTML Default Form Submit)
event.preventDefault();
// Event: Prevents Event Bubbling To Parent Elements
event.stopPropagation();
// const reference = <HTMLFormElement>formRef.current;
console.log('WHY IS THE FORM VALID???');
console.log(formRef.current.checkValidity());
// Check Form Validity
if (formRef.current.checkValidity() === true) {
// Validate Form
setValidated(true);
// Redux: Send Verification Code Request
dispatch(sendVerificationCodeRequest(email));
}
else {
// Do Nothing (Form.Control.Feedback Will Appear)
console.log('DO NOTHING');
}
};
As the error says, the problem is that the ref could be null — and in fact, that's what you're initializing it to. That means formRef.current may be null. Which means formRef.current.checkValidity() needs a check for whether formRef.current is null.
You can use &&:
if (formRef.current && formRef.current.checkValidity()) {
// ^^^^^^^^^^^^^^^^^^^
or the new optional chaining operator:
if (formRef.current?.checkValidity()) {
// ^
Side note: There's almost¹ never any reason for === true or === false, and certainly none above. checkValidity returns a boolean, so you already have a boolean to test with the if.
// Idiomatically:
if (x) { // Is this true?
// ...
}
if (!x) { // Is this false?
// ...
}
¹ "almost" - The only time it really makes sense is if what you're testing may not be a boolean at all and you want the check to result in false if it isn't, which is a rare edge case.
You must somehow ensure formRef.current is not null, because it could be. One way is to add this in the beginning of the callback:
if(!formRef.current) return
Also you could use the optional chaining operator, although in this case if formRef.current turns out to be null, the DO NOTHING part of code will be triggered
if(formRef.current?.checkValidity() === true)
I am doing an axios.get call on an API, in React. When I run it the first time, I get the results that I expect. When I run it the next time and search for the same thing, I get more results than I had originally. Then if I search again, for the same thing, I get even more results that I had the second time. It keeps adding new results to the old ones without starting a brand new search. If I search for a different keyword, then I get results with both the new keyword results and the old ones that I did before.
There is probably an easy fix to this, but how do I get it to discard the old results and create new ones? I have tried creating CancelTokens, but haven't been successful.
I am currently putting the results into a new array and I set that array equal to nothing when I first render the component, so I don't understand how this problem is happening in the first place.
Thanks for any help you can offer!
async componentDidMount() {
//searches the api for the hashtag that the user entered
newArray.length=0;
newArray=[];
await axios.get(`https://laffy.herokuapp.com/search/${this.props.toSearch}`).then(function(response) {
returnedKeywordSearch = response.data;
}) //if the api call returns an error, ignore it
.catch(function(err) {
return null;
});
//goes through the list of locations sent from the api above and finds the latitude/longitude for each
var count = 0;
while (count < returnedKeywordSearch.length) {
locationToSearch = returnedKeywordSearch[count].location;
if (locationToSearch !== undefined) {
var locationList = await axios.get(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationToSearch}.json?access_token=pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg`)
.catch(function(err) {
return null;
});
if (locationList !== null) {
if (Array.isArray(locationList.data.features) && locationList.data.features.length)
{
locationCoordinates.push(locationList.data.features[0].center);
if (returnedKeywordSearch[count].location!== null && returnedKeywordSearch[count].location!==""
&& locationList.data.features[0].center !== undefined)
{newArray.push({
id: returnedKeywordSearch[count].id,
createdAt: returnedKeywordSearch[count].createdAt,
text: returnedKeywordSearch[count].text,
name: returnedKeywordSearch[count].name,
location: returnedKeywordSearch[count].location,
coordinates: locationList.data.features[0].center
});
}
}
}
}
count++;
}
//tweetSpots is set to null in the initial component state set up
this.setState({tweetSpots: newArray});
this.setState({ done: true}); //sets done to true so that loading animation goes away and map displays
}
Okay, I figured out this problem is actually on the server side in Express where the data isn't being reset. So, I'll close this one and open a question about Express.
I'm getting files from my input: other = event.target.files; which starts as a global variable and console.logs as FileList {0: File, 1: File, 2: File, 3: File, length: 4}. I'm adding this to my state(this.state.songList, which starts as null) and if(this.state.songList != null) I'm adding the list of songs from this.state.songList to my FormData.
.then(()=> {
this.setState({songsList : other})
}).then(()=> {
if(this.state.songList != null) {
Array.from(this.state.songsList).forEach(song => {
data.append('songs', song);
})
}
console.log(other);
console.log(this.state.songList)
})
For some reason the code inside the if statement almost never gets ran because this.state.songList is always undefined in the console. If I run the code in the second .then without the if statement it gets ran and adds my files to the FormData so I know for a fact that this.state.songList is not empty in my second .then.
Why does the code in the second .then not get ran when the if(this.state.songList != null) is surrounding it although it gets ran without so I know it doesn't equal null and what can I do to fix this issue of state getting read as null.
You're running into this issue because setState is an async function that won't necessarily update by the time your then executes. You should use the callback function for setState to do whatever you need to do with the new value.
this.setState({
songlist: other
}, () => {
// code to execute after the songs update in state
if(this.state.songList != null) {
Array.from(this.state.songsList).forEach(song => {
data.append('songs', song);
})
}
})
Hopefully that helps!
I have the following object, from which I want to remove one comment.
msgComments = {
comments: [
{ comment:"2",
id:"0b363677-a291-4e5c-8269-b7d760394939",
postId:"e93863eb-aa62-452d-bf38-5514d72aff39" },
{ comment:"1",
id:"e88f009e-713d-4748-b8e8-69d79698f072",
postId:"e93863eb-aa62-452d-bf38-5514d72aff39" }
],
email:"test#email.com",
id:"e93863eb-aa62-452d-bf38-5514d72aff39",
post:"test",
title:"test"
}
The action creator hits the api delete function with the commentId:
// DELETE COMMENT FROM POST
export function deleteComment(commentId) {
return function(dispatch) {
axios.post(`${API_URL}/datacommentdelete`, {
commentId
},{
headers: { authorization: localStorage.getItem('token') }
})
.then(result => {
dispatch({
type: DELETE_COMMENT,
payload: commentId
});
})
}
}
My api deletes the comment and I send the comment id to my Reducer, this is working fine to this point, api works and comment is deleted. The problem is updating the state in the reducer. After much trial and error at the moment I am trying this.
case DELETE_COMMENT:
console.log('State In', state.msgComments);
const msgCommentsOne = state.msgComments;
const msgCommentsTwo = state.msgComments;
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload );
const newComments = [
...msgCommentsTwo.data.comments.slice(0, deleteIndexComment),
...msgCommentsTwo.data.comments.slice(deleteIndexComment + 1)
];
msgCommentsOne.data.comments = newComments;
console.log('State Out', msgCommentsOne);
return {...state, msgComments: msgCommentsOne};
Both state in AND state out return the same object, which has the appropriate comment deleted which I find puzzling.
Also the component is not updating (when I refresh the comment is gone as a new api call is made to return the updated post.
Everything else seems to work fine, the problem seems to be in the reducer.
I have read the other posts on immutability that were relevant and I am still unable to work out a solution. I have also researched and found the immutability.js library but before I learn how to use that I wanted to find a solution (perhaps the hard way, but I want to understand how this works!).
First working solution
case DELETE_COMMENT:
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload);
return {
...state, msgComments: {
data: {
email: state.msgComments.data.email,
post: state.msgComments.data.post,
title: state.msgComments.data.title,
id: state.msgComments.data.id,
comments: [
...state.msgComments.data.comments.slice(0, deleteIndexComment),
...state.msgComments.data.comments.slice(deleteIndexComment + 1)
]
}
}
};
Edit:
Second working solution
I have found a second far more terse solution, comments welcome:
case DELETE_COMMENT:
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload);
return {
...state, msgComments: {
data: {
...state.msgComments.data,
comments: [
...state.msgComments.data.comments.slice(0, deleteIndexComment),
...state.msgComments.data.comments.slice(deleteIndexComment + 1)
]
}
}
};
That code appears to be directly mutating the state object. You've created a new array that has the deleted item filtered out, but you're then directly assigning the new array to msgCommentsOne.data.comments. The data field is the same one that was already in the state, so you've directly modified it. To correctly update data immutably, you need to create a new comments array, a new data object containing the comments, a new msgComments object containing the data, and a new state object containing msgComments. All the way up the chain :)
The Redux FAQ does give a bit more information on this topic, at http://redux.js.org/docs/FAQ.html#react-not-rerendering.
I have a number of links to articles talking about managing plain Javascript data immutably, over at https://github.com/markerikson/react-redux-links/blob/master/immutable-data.md . Also, there's a variety of utility libraries that can help abstract the process of doing these nested updates immutably, which I have listed at https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md.
I am call a REST service in my React component's componentDidMount:
loadData: function () {
return $.getJSON(this.props.server+"/xxxxx/"+this.props.params.promotionid);
},
componentDidMount: function () {
this.loadData().success(function (data) {
console.log("After success");
console.log(data);
if(this.isMounted())
{
this.setState({
prom: data
//release: data.Release
});
}
}.bind(this))
}
This works even without documentation suggestion to include an if(this.isMounted()), there. Plesee note the commented line. We will need to comment this out in order to work. The thing is that if the data I get from the server has a subobject in it, it does not work even with the isMounted(). eg:
{
PromotionId: 1,
Description: "Hello three",
Category: "serviceid",
SpecialID: 23666,
ProjectManager: "Tarkidi Touvouda",
Requestor: "George Klapas",
Readiness: false,
SignOff: false,
SourceDestination: "developement-uat",
Status: "pending",
ReleaseId: 2,
Release: {
ReleaseId: 2,
ReleaseName: "Fall Second",
ReleaseDate: "2015-10-05T00:00:00",
ReleaseDeadline: "2015-10-05T00:00:00",
ModifiedDate: "2015-10-21T12:48:45.753"
},
PromotionAssets: null,
ModifiedDate: "2015-10-21T12:48:45.753"
}
I get the data at render() as follows
var p=this.state.prom;
var rel=p.Release;
I get an error when I try to access rel.ReleaseName that cannot find Releasename of undefined. The only way to make it work is removing the comment you saw (that is to say, having a separate field in state that will hold Release object) and then :
var rel=this.state.release;
Subsequent calls to Release object are successful. Why should I do that? I mean have a separate state field for this?
$.getJSON is an async call, which means that the execution continues while waiting for the response. So your script calls componentDidMount and after that continues with render() without waiting for the results.
Because of this in the first round of render(), the variable this.state.prom is undefined. Due to this a call to this.state.prom.Release throws an error, because you cannot get a property of an undefined object.
A simple solution might be to wait until the AJAX call has been finished an the state has been updated:
render: function() {
if(!this.state.prom) {
return <div>loading ...</div>;
}
// your code goes here
}