So this is my code for component did update and this.props.getQuestions() is constantly firing. questions property is an array of questions which takes the values of the global state.
componentDidUpdate(prevProps){
if(prevProps.questions !== this.props.questions){
this.props.getQuestions();
}
}
Connecting state to props
const mapStateToProps = state => ({
questions: state.question.questions
});
When I try to compare the length of array between prevState and the current state, the method is never fired.
componentDidUpdate(prevProps){
if(prevProps.questions.length !== this.props.questions.length){
this.props.getQuestions();
}
}
How to make it work, so this method is fired only when the array is updated?
Simple array compare doesn't work. It will always match your condition ie. always not equal. And thus, componentDidUpdate fires constantly. Rather, you should check it like:
if(JSON.stringify(prevProps.questions) !== JSON.stringify(this.props.questions)){
this.props.getQuestions();
}
Regarding the checking with length property, it should work fine. But you stated that it isn't working. It seems you get same array when you use getQuestions. Check that function properly that it gets updated questions.
Your condition for fire was not right which you can see with your comparison of length of the array. You are checking for the equality of array's reference which is anyways incorrect(not sure how redux handles the prop update though).
You drill down to specificity of the questions in comparison and can compare questions' length, questions' questionId etc.. Loop through them and identify as desired.
You can even use library like lodash https://lodash.com/docs to compare array's and specific property.
If questions is the only property in mapStateToProps() you could just ensure you create a new array wherever you update it
newState.question.questions = [...updatedArray];
and remove the if statement from your componentDidUpdate entirely.
Make sure you do not update the questions array when calling getQuestions(), doing that can cause infinite loops where your didUpdate function causes an update (triggers didUpdate, ad infinitum)
Related
In my React app I purposely combined several arrays in one state using a useState hook. These arrays represent visual objects that I would like to keep "managed" together to ensure that that re-renderings of my application are visually consistent.
While testing I tried to change a property of some objects of my first array. updatedElements reflects the update properly (clearly shown by my console-log). However, updating my useState state does not work. The array elements does not change at all.
Here is the relevant code:
const updatedElements: VisualDiagramElementData[] =
visualData.elements.map((element: VisualDiagramElementData) =>
element.id === id
? { ...element, selected: true }
: { ...element, selected: false }
);
console.log(updatedElements);
setVisualData({
elements: updatedElements,
connectors: visualData.connections,
connections: visualData.connections,
});
What am I missing / doing wrong? Any help is highly appreciated!
When updating a state property based on its previous value, the callback argument of the state setter should be used. Otherwise you may use stale state values.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
https://reactjs.org/docs/hooks-reference.html#functional-updates
It should be:
setVisualData((visualData)=>({
elements: visualData.elements.map(/*Your map callback*/),
connectors: visualData.connections,
connections: visualData.connections,
}));
I would go with what is recommended in the React docs - split them out into multiple state variables:
https://reactjs.org/docs/hooks-faq.html#should-i-use-one-or-many-state-variables.
Depending on how you trigger the state updates will determine whether re-render occurs multiple times or not.
See this answer for more details on how the batching works:
https://stackoverflow.com/a/53048903/6854709
Also take note of the comment by aprillion:
Note from github.com/facebook/react/issues/10231#issuecomment-316644950 - This is implementation detail and may change in future versions.
Eventually, it worked by using the "callback argument of the state setter". I revised / reviewed my entire code and I modified all my state setters accordingly. Now, everything works like a charm. Thank you very much again for your help, especially to Nice Books.
I'm working on a code where I have an empty array which has to be filled after a certain request returns.
I tried to setState of the array like that:
const [recentTransactionContacts,setRecentTransactionContacts] = useState<object[]>([])
///// more code
Contacts.getAll((err, contacts) => {
if(err) throw err
if(contacts.length > 0)
setRecentTransactionContacts(prevState => ({...prevState.recentTransactionContacts, ...contacts}));
else
ToastAndroid.show(`There no contacts on your phone. Can't transfer`, ToastAndroid.SHORT)
})
Now, I don't see any change in the array of contacts after trying to do setState that way.
The only way, that I could update the state was like that:
setRecentTransactionContacts(contacts)
However, I don't think that the latter is the correct way.
What is the correct way to update the state of an array?
I think this should be served your case, the recentTransactionContacts is not changed after this line runs so you do not need to access to prevState to do what you want.
setRecentTransactionContacts([...recentTransactionContacts, ...contacts]);
Using prevState or updater function is required in cases where the current state might be changed or stale, which doesn't seem to be the case here. Infact it could be the prevState itself causing issues, as the prevState value could be undefined at run.
As stated, replace your method to
setRecentTransactionContacts([...recentTransactionContacts, ...contacts]);
which will take in the current state value and append the contacts to the state.
I have created the following demo to help me describe my question: https://codesandbox.io/s/dazzling-https-6ztj2
I have a form where I submit information and store it in a database. On another page, I retrieve this data, and set the checked property for the checkbox accordingly. This part works, in the demo this is represented by the dataFromAPI variable.
Now, the problem is that when I'd like to update the checkboxes, I get all sorts of errors and I don't know how to solve this. The ultimate goal is that I modify the form (either uncheck a checked box or vice versa) and send it off to the database - essentially this is an UPDATE operation, but again that's not visible in the demo.
Any suggestions?
Also note that I have simplified the demo, in the real app I'm working on I have multiple form elements and multiple values in the state.
I recommend you to work with an array of all the id's or whatever you want it to be your list's keys and "map" on the array like here https://reactjs.org/docs/lists-and-keys.html.
It also helps you to control each checkbox element as an item.
Neither your add or delete will work as it is.
Array.push returns the length of the new array, not the new array.
Array.splice returns a new array of the deleted items. And it mutates the original which you shouldn't do. We'll use filter instead.
Change your state setter to this:
// Since we are using the updater form of setState now, we need to persist the event.
e.persist();
setQuestion(prev => ({
...prev,
[e.target.name]: prev.topics.includes(e.target.value)
// Return false to remove the part of the array we don't want anymore
? prev.topics.filter((value) => value != e.target.value)
// Create a new array instead of mutating state
: [...prev.topics, e.target.value]
}));
As regard your example in the codesandbox you can get the expected result using the following snippet
//the idea here is if it exists then remove it otherwise add it to the array.
const handleChange = e => {
let x = data.topics.includes(e.target.value) ? data.topics.filter(item => item !== e.target.value): [...data.topics, e.target.value]
setQuestion({topics:x})
};
So you can get the idea and implement it in your actual application.
I noticed the problem with your code was that you changed the nature of question stored in state which makes it difficult to get the attribute topics when next react re-renders Also you were directly mutating the state. its best to alway use functional array manipulating methods are free from side effects like map, filter and reduce where possible.
locationHistory is always an empty array in the following code:
export function LocationHistoryProvider({ history, children }) {
const [locationHistory, setLocationHistory] = useState([])
useEffect(() => history.listen((location, action) => {
console.log('old state:', locationHistory)
const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
setLocationHistory(newLocationHistory)
}), [history])
return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}
console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.
Any advice would be much appreciated.
UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.
Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?
P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)
You're setting up a listener for the history object, right?
Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:
Set up the listener, after 1st render (i.e: after mounting)
Remove the listener, after unmount
For this you could do it like this:
useEffect(()=>{
history.listen(()=>{//DO WHATEVER});
return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]); // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER
But if your history object will change on every render, you'll need to:
cancel the last listener (from the previous render) and
set up a new listener every time your history object changes.
useEffect(()=>{
history.listen(()=>{//DO SOMETHING});
return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]); // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT
NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.
But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.
setLocationHistory((prevState) => {
if (prevState.length > 0) {
// DO WHATEVER
}
return SOMETHING; // I.E.: SOMETHING WILL BE YOUR NEW STATE
});
I have a form with multiple controls that saves everything to a variable. Each control has an onChanged function, which runs a state update with that control's new value:
function onChangedValUpdate(newVal){
let fields = clone(this.state.fields);
fields[controlId] = newVal;
this.setState({fields});
}
My controls are dynamically created, and when they are, they run onChangedValUpdate on their initial value, if one is present. The problem is, sometimes a lot of controls are created at once, and React queues up its setStates using the same cloned fields object for each update. The object is not updated between setStates, presumably for similar reasons to this question. This means that, effectively, all but one control's updates are overwritten.
I tried writing an over-smart routine which used setState's callback to only run it if there isn't one already in progress and remember changes made to the fields variable in between setStates, but React went and ran all my queued updates simultaneously. Regardless, the routine felt too contrived to be right.
I'm sure this is a trivial and solved problem, but I can't seem to formulate my question in a Googleable way. How do I chain state updates that happen concurrently, and of which there may be any number?
EDIT For posterity, my solution, thanks to Yuri:
function onChangedValUpdate(newVal){
this.setState( state => {
let fields = clone(state.fields);
fields[controlId] = newVal;
return {fields};
}
}
You could pass a mutation function to setState. This will prevent overwritting on batched updates because every callback will get the most recent previous state.
function onChangedValUpdate(newVal){
this.setState(function(state){
const fields = clone(state.fields)
fields[controlId] = newVal
return {fields: fields}
});
}
Or using object spread and enhanced object literals.
function onChangedValUpdate(newVal){
this.setState(({fields}) => ({fields: {...fields, [controlId]: newVal}}));
}