React props change on setState - reactjs

Problem: using setList is updating the component props and the state inside the component, not just the state inside the component.
I have worked out a partial solution, cloning the instance works the first time round, and I stopped using splice, see addPhaseToList function at the bottom.
New problem: After cloning the Phase object the list goes back to how it was in props.
In props it had length 5, ['Phase 1','Phase 2','Phase 3','Phase 4','Phase 5'],
Added something ['Phase 1','Phase 2','Phase 3','Phase 4','Test','Phase 5'],
Added something else ['Phase 1','Phase 2','Phase 3','Phase 4','Test 2','Phase 5'].
Console.log reveals after cloning it goes back to. ['Phase 1','Phase 2','Phase 3','Phase 4','Phase 5']
public insertPhase = (phase:string):Phases => {
let clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this)
console.log('clone a', clone)
this.addPhaseToList(phase,clone)
return clone;
private addPhaseToList = (phase:string,phaseObj:Phases) => {
let result:string[] = [];
for (let i=0;i<phaseObj.phaseList.length;i++) {
if (i === phaseObj.phaseList.length-1) {
result = result.concat([phase])
}
result = result.concat([phaseObj.phaseList[i]])
}
phaseObj.phaseList = result;
Any help would be amazing!
Cheers.

Related

How to correctly modify the state from the parent component before passing it down to child component

I have three components:
1st component
--2nd component
----3rd component
I need to pass a state and its handleState down from the first component to the second one. In the second one I need to filter through the state array values, I also need to update the db and the state so I make multiple API and handleState calls in a for loop inside of useEffect(). The third one renders the state data.
Here's the second component:
export default function LearnNewApp({ deck, handleShowAppChange, handleDecksChange }) {
let words = deck.words.filter(x => x.wordGroup == "newLearning")
useEffect(() => {
let unsubscribed = false;
if (words.length < 10) {
let vacant = 10 - words.length
let newDeck = JSON.parse(JSON.stringify(deck))
let unseenWords = newDeck.words.filter(x => x.wordGroup === "newUnseen")
for (let i = 0; i < vacant && i < unseenWords.length; i++) {
let wordUnseenToLearning = unseenWords[i]
wordUnseenToLearning.wordGroup = "newLearning"
callAPI(wordUnseenToLearning.id, "newLearning")
if (!unsubscribed) handleDecksChange(wordUnseenToLearning)
}
}
return () => unsubscribed = true;
}, [])
function memorized(word) {
callAPI(word.id, "first")
let wordLearningToFirst = {
...word, wordGroup: "first"
}
handleDecksChange(wordLearningToFirst)
}
function showAgain() {
}
if (words != []) return (
<CardPage words={words} handleShowAppChange={handleShowAppChange} leftButtonFunc={memorized} rightButtonFunc={showAgain} />
)
}
The state array that's being modified is an array of decks with words. So I'm trying to narrow down the chosen deck with .filter to up to ten words so they could be shown to the user in the third component. To do that I check how many words there are with showNew attribute. If there's less than ten of them I check if there are any new words in the deck to change from neverShown to showNew.
The code causes an error probably because it takes some time to do everything in useEffect(), not to mention it runs after render.
The third component has a button that triggers a function from the second component that also updates the db and state but not in a loop.
So the main problem is that I don't know how to properly fix the deck modification and subsequent render in the second component.

MobX displaying previous query's results when this query errors

When a timeout error occurs while retrieving data from an API to render on a MobX-enabled React page, I am seeing the results from the last successful query displayed in place of the desired data (or empty data). Specifically, the steps are:
Enter a page that requires an item Id to retrieve results from a database and puts query results in state for display
Go back, enter the same page with a new Id, this request times out. Instead of seeing nothing or an error, I am seeing the results from step 1, i.e. the wrong item data.
This is happening site-wide, and I need a fix I can implement everywhere. Below is some code I wrote to fix the problem on one page, but this pattern will need to be copied into every store in our app. I'm not confident it's the right solution, because it works by tracking an item Id and emptying all observables when there's a change - this feels like something MobX should be doing, so I'm afraid my solution is an anti-pattern.
Is there a better solution to this problem than the one I'm presenting below?
class SupplierUtilizationStore {
#observable key = 0; //supplierId
utilizationSearchStore = new SearchStateStore();
#observable utilizationSearchResults = [];
#observable selectedChartType = 'ByMonth';
#observable supplierUsageForChart = {};
#observable utilizationSummaryData = {};
constructor(rootStore, dataLayer) {
this.rootStore = rootStore;
rootStore.supplierStore = this;
this.db = dataLayer;
this.initUtilizationSearchStore();
}
initUtilizationSearchStore() {
this.utilizationSearchStore.searchResultsTotalUnitCost = observable({});
this.utilizationSearchStore.searchResultsTotalCost = observable({});
this.utilizationSearchStore.searchResultsTotalQty = observable({});
this.utilizationSearchStore.supplierId = observable({});
}
//Call this in componentDidMount()
#action
initStore(key) {
if (key !== this.key) {
this.utilizationSearchStore = new SearchStateStore();
this.initUtilizationSearchStore();
this.utilizationSearchResults = [];
this.selectedChartType = 'ByMonth';
this.supplierUsageForChart = {};
this.utilizationSummaryData = {};
this.utilizationSearchStore.supplierId = key;
this.key = key;
}
}
...
}
As I understand "key" is id, which value depends on user action. So you can change it when a page is opened and observe the change to reset all values.
It doesn't seem correct to me to reset/init every single value of another object. Instead create a method for doing that in the exact object(SearchStateStore should have its own init method).
I haven't changed that part, but I am not sure it is also correct architectural behavior to pass the root store to the child store and assign the child store to the parent inside itself(this is not a mater of the question. Just something you can think of)
After all the code looks like this:
class SupplierUtilizationStore {
#observable key = 0; //supplierId
utilizationSearchStore;
#observable utilizationSearchResults;
#observable selectedChartType = 'ByMonth';
#observable supplierUsageForChart;
#observable utilizationSummaryData;
constructor(rootStore: any, dataLayer:any) {
this.rootStore = rootStore;
rootStore.supplierStore = this;
this.db = dataLayer;
this.initStore();
observe(this.key,()=>{ this.initStore() });
}
#action
initStore() {
this.utilizationSearchStore = new SearchStateStore();
this.utilizationSearchStore.init(); // all logic for initialization should be in the concrete store
this.utilizationSearchResults = [];
this.selectedChartType = 'ByMonth';
this.supplierUsageForChart = {};
this.utilizationSummaryData = {};
this.utilizationSearchStore.supplierId = this.key;
}
}
...
}
This way, you don't have to interact with the store and tell it when to init/reset. Just change the observable "key" value from the react component and the store will know what to do.

Detect handle change inside componentDidUpdate method - Reactjs

I have a form that contains several questions. Some of the questions contains a group of subquestions.
The logic to render sub questions is written inside componentDidUpdate method.
componentDidUpdate = (prevProps, prevState, snapshot) => {
if (prevProps !== this.props) {
let questions = this.props.moduleDetails.questions,
sgq = {};
Object.keys(questions).map((qId) => {
sgq[qId] = (this.state.groupedQuestions[qId]) ? this.state.groupedQuestions[qId] : [];
let answerId = this.props.formValues[qId],
questionObj = questions[qId],
groupedQuestions = [];
if(questionObj.has_grouped_questions == 1 && answerId != null && (this.state.groupedQuestions != null)) {
groupedQuestions = questions[qId].question_group[answerId];
let loopCount = this.getLoopCount(groupedQuestions);
for(let i=0; i<loopCount; i++) {
sgq[qId].push(groupedQuestions);
}
}
});
this.setState({groupedQuestions: sgq});
}
}
The problem is that on every key stroke of text field, handleChange method is invoked which will ultimately invoke componentDidUpdate method. So the same question groups gets rendered on every key stroke.
I need a way to detect if the method componentDidUpdate was invoked due to the key press(handleChange) event so that i can write logic as follows.
if(!handleChangeEvent) {
Logic to render question group
}
Any idea on how to integrate this will be appreciated.
I assume your textfield is a controlled component, meaning that its value exists in the state. If this is the case, you could compare the previous value of your textfield to the new one. If the value is different, you know the user entered something. If they are equal however, the user did something else at which point you want your snippet to actually execute.
Basically:
componentDidUpdate = (prevProps) => {
// if value of textfield didn't change:
if (prevProps.textfieldValue === this.props.textfieldValue) {
// your code here
}
}
Another approach is to use componentDidReceiveProps(). There you can compare the props to the previous ones, similarly to the above, and execute your code accordingly. Which method is most suitable depends on how your app works.

Pushing element in a cloned object of state adds value to the state attribute instantaneously

I am trying to publish an updated object but not trying to change the state through the following code in react:
addPlaceToSearch = (place) => {
if (place != this.state.search_text) {
var search = $.extend({}, this.state.search);
if (!search.added_places.includes(place)) {
search.added_places.push(place);
PubSub.publish('searchUpdated', search);
}
}
}
Thus, I am using $.extend({}, this.state.search) to clone the object into another variable, modify that particular variable and then publish it. But when I execute the line 5, and I put a breakpoint on line 6, I can see that the this.state.search is also changed with place being pushed in it's added_places key (which I do not want). How is this happening? And how can I prevent it? I have also tried Object.assign({}, this.state.search) instead of $.extend({}, this.state.search).
EDIT
I even tried the most trivial solution, here it is:
addPlaceToSearch = (place) => {
if (place != this.state.search_text) {
if (!this.state.search.added_places.includes(place)) {
var xyz = {};
for (var key in this.state.search) {
xyz[key] = this.state.search[key];
}
xyz.added_places.push(place);
PubSub.publish('searchUpdated', xyz);
}
}
}
Still, the xyz.added_places.push(place) line changes my this.state.search object too! Kindly help me out.
Finally, after two hours of hard work, I figured out the solution by making a deep copy of the original array. I will read about the differences between shallow and deep copy as given in this answer, till then here is the solution:
var xyz = $.extend(true, {}, this.state.search);
xyz.added_places.push(place);
You can do this better without jQuery using Object.assign()
var xyz = Object.assign({}, this.state.search)
xyz.added_places.push(place)

React change state in array (for loop)

I have a State with flights, and I have a slider to change the max price to change visibility of the flight elements.
maxpriceFilter() {
var flightOffer = this.state.flightOffer;
var sliderPrice = this.state.sliderPrice;
for (var i = 0; i < flightOffer.length; i++) {
if( flightOffer[i].price > sliderPrice) {
this.setState(
{[flightOffer[i].hiddenprice] : true}
);
};
}
This code is adding a undefined field with status true in the root of the state though.. I cant find any best practice on this, other then using computed fields. But I cant get the computed field working either..
Could someone please help me out here?
You don't want to do a setState call in a loop, that will have the react component render multiple times. Build a new state object and call the setState once. You also don't want to filter it out by an if statement, but set previous values to hidden:
maxpriceFilter() {
var flightOffer = this.state.flightOffer;
var sliderPrice = this.state.sliderPrice;
var newState = {};
for (var i = 0; i < flightOffer.length; i++) {
newState[flightOffer[i].hiddenprice] = flightOffer[i].price > sliderPrice;
}
this.setState(newState);
// ...
}
If you're still having issues, it could be that the hiddenprice property isn't what you expect? Might need to post your render() function as well.
Instead of doing your looping when you're updating the state, why not just do the switch on render?
You're probably already looping over all flightOffers in render(). So, just add the check there. Inside your render, pass hiddenPrice={offer.price > sliderPrice} as a prop, or use it directly where you control the visibility.
Why? Because the visibility of a specific item in this case is not state. It is a result of the state.

Resources