Yesterday I had a behaviour that I don't understand by using Immer.js and setState together. I was using a setState (in a bad way, by the way) when fetching my data and this fetch was called at each endReached of my SectionList.
This setState looked like this:
this.setState((prev) => {
let sections = prev.sections
/* Extract alive topics from "topics"(array retrieved from fetch)*/
let topicsSection1 = topics.filter((card) => !card.states.is_killed)
/* Extract killed topics from "topics"(array retrieved from fetch)*/
let topicsSection2 = topics.filter((card) => card.states.is_killed)
if (sections[0] && sections[0].data)
sections[0].data = positionExtracted > 1 ? sections[0].data.concat(...topicsSection1) : topicsSection1
if (sections[1] && sections[0].data)
sections[1].data = positionExtracted > 1 ? sections[1].data.concat(...topicsSection2) : topicsSection2
return {
sections: sections,
position: response.position,
lastPage: response.laftPage
}
})
and everything worked just fine.
However, I have a function that is called when you press on the topic, and it changes the "opened" value of the topic in the data array to indicate to the list that "this topic" is open.
This function calls the "produce" function of Immer.js
And this function looks like this:
_onPressTopic = (id_card) => {
this.setState(produce((draft) => {
if (draft.sections[0] && draft.sections[0].data)
draft.sections[0].data = draft.sections[0].data.map((item) => {
if (item.id === id_card)
item.opened = !item.opened
return item
})
if (draft.sections[1] && draft.sections[1].data)
draft.sections[1].data = draft.sections[1].data.map((item) => {
if (item.id === id_card)
item.opened = !item.opened
return item
})
}))
}
MY PROBLEM IS:
If I open a topic and then my list data goes through this function, then when an endReached is called again, either I get an error "This object is not expensive", or my list data is not modified at all. And if instead of my first setState, I use a produce from Immer, everything works again.
What I don't understand is: Why does everything work perfectly if I only use Immer.js or just SetState, but as soon as I try to use both together, they don't seem to get along?
Thank you for your answers,
I hope I made it clear !
Viktor
Related
I'm in the midst of cleaning up errors for a repo and I've come across this error where someone's trying to to assign a tag value object to a const variable inside of a map function. Here's its current form:
const BatchEditState = {
CURRENT: 'CURRENT',
DELETE: 'DELETE',
PUT: 'PUT',
}
handleShow = () => {
this.batchEditSet = {};
this.state.currentTags.map((tag) => {
this.batchEditSet[tag.tag_name] = BatchEditState.CURRENT;
});
};
As far as I've researched, one is definitely not supposed to go about it this way even if it does still function. I've seen plenty examples returning a jsx element, but I'm pretty sure that's not the point for this. I do know a map function is supposed to at least return a value however.
I attempted to use a spread operator and an implicit return, but that didn't work out. I also tried making a basic return & even though I'm not encountering any immediate errors in our application, I'm still not sure if this is the right way to go. Still fairly new at this, but appreciate any info, help, and education I can get
handleShow = () => {
this.batchEditSet = {};
this.state.currentTags.map((tag) => {
this.batchEditSet[tag.tag_name] = BatchEditState.CURRENT;
return(
BatchEditState.CURRENT
)
});
};
.map is only for creating new arrays by iterating over an existing array. While you want to iterate over an existing array, you don't want to create a new array - rather, you want to construct a plain object - so .map should not be used here. (The array you're constructing in your current code is going unused.)
To procedurally assign to properties of the object, do:
handleShow = () => {
this.batchEditSet = {};
this.state.currentTags.forEach((tag) => {
this.batchEditSet[tag.tag_name] = BatchEditState.CURRENT;
});
};
Or create an array of entries, then turn that array into an object.
handleShow = () => {
this.batchEditSet = Object.fromEntries(
this.state.currentTags.map(tag => [tag.tag_name, BatchEditState.CURRENT])
);
};
But also, doing this.batchEditSet = in the first place looks like a mistake in React. If this is a component, you should almost certainly be calling this.setState instead of mutating the instance.
This is kind of complicated to explain. I'm trying to make a toggle function that adds and deletes items on a different page with useContext. Everything adds and deletes fine until I tab to the other page, and that's where the error begins. Once I do that the function ignores what's in the array and will duplicate items in the array. What's odd about it is if I console.log or manually check the new item with the current array items it shows me everything. For example in order to add the new item to the array it checks if the index of new item is -1. If it is it will add the item and if not it will delete the item. However once I leave the page it doesn't see the item anymore and adds it anyway. If I console.log the item name and new item name, I can see both, and if I use === to check it also works fine until I switch tabs and then even though it's still console logging both names somehow it's still adding the item and ignoring that it already contains the item.
The code directory in my sandbox is src/Helpers/MyPicsContext. Here is the link to my sandbox codesandbox
The tabs on the website are Picks and the search page which you can access on Picks before any items are added or by clicking the magnifying glass in the top right of page.
And here is the actual code for the context page.
export const MyPicksContextProvider = props => {
const [picksList, setPicksList] = useState(
JSON.parse(localStorage.getItem('picksList'))
||
[]
)
//console.log(picksList)
useEffect(() => {
localStorage.setItem("picksList", JSON.stringify(picksList));
}, [picksList]);
const deleteCoin = coin => {
if (picksList.length === 1) {
setPicksList([]);
} else {
setPicksList(picksList.filter(list => {
console.log(list)
//console.log(coin)
return list !== coin;
}))
}
console.log('deleted');
console.log(picksList)
}
const toggleCoin = (coin) => {
if (picksList.length === 0) {
setPicksList([...picksList, coin]);
} else {
if (picksList.indexOf(coin) === -1 ) {
setPicksList([...picksList, coin]);
console.log('added 1')
} else {
deleteCoin(coin)
}
}
}
Perhaps I just don't understand useState and prevState, but I can't seem to find any examples that apply to what I'm trying to do here. It makes total sense in creating a counter or something simple like that.
The issue is that .indexOf checks for referential equality of items, same as using ===. This means that const a = {id: 'x'}; const b = a; console.log(a === b); will output true, but const a = {id: 'x'}; const b = {id: 'x'}; console.log(a === b); will output false.
In your situation, upon changing pages / refreshing, the state is reset and loaded from local storage. However, this creates new objects which are not referentially equal. Instead of using .indexOf you want to use .find, something like array.find((element) => element.id === newItem.id) to find the index of the item. You could also do your own deep equality check (confirming every field matches), but I suspect the ID alone is sufficient.
In fact, I would also recommend only keeping the array of "picks" as an array of string ID's. Then you can lookup the full data from your table for each of these ID's. Otherwise the current price will be stored in localStorage, and could be out of date.
I have updated this question with clearer and more concise code on 15/03/22.
I have a text filter and it filters an array (displaying a list) correctly if it matches something in the displayed list, but as the user deletes character by character the filter does not change. I need it to keep matching as the user delete's chars.
Here is the filter which is set as the onChange for the text field:
const [searchInputTitle, setSearchInputTitle] = useState<string>('');
const [filteredItemsArrayState, setFilteredItemsArrayState] = useState<IListItems[]>(props.requests);
const searchTitles = () => {
let filteredArrayByTitle: IListItems[] = [];
filteredArrayByTitle = theRequestsState.filter((item) => {
return item.Title && item.Title.toLowerCase().indexOf(searchInputTitle.toLowerCase()) >= 0;
});
console.log(searchInputTitle, 'searchInputTitle');
if (searchInputTitle && searchInputTitle.length > 0) {
setTheRequestsState(filteredArrayByTitle);
setIsFiltered(true);
} else if (searchInputTitle && searchInputTitle.length === 0) {
const AllItems = props.requests;
let sortedByID: IListItems[] = AllItems.sort((a, b) => a.Id > b.Id ? 1 : -1);
setTheRequestsState(sortedByID);
setIsFiltered(false);
}
};
<TextField
onChange={searchTitles}
value={searchInputTitle}
/>
useEffect(() => {
_onTitleFilterChange(null, searchInputTitle);
if (isFiltered == true) {
setFunctionalArea(null, null);
setRequestingFunction(null, null);
}
}, [isFiltered, searchInputTitle]);
////////
<DetailsList className={styles.DetailsList}
items={filteredItemsArrayState.slice((ListPage - 1) * 50, ((ListPage * 50)))}
/>
Can anyone see why the render is not updating on deletion of char and what I could use to do so?
Update: As I type a character into the search I can see it's finding the searched for char/word and also if I delete chars now it searches and finds actively, but as soon as I stop typing it reverts back to the original array of items.
Can you try to filter the array you give to the DetailsList so you never lost the data ..?
filteredItemsArrayState.filter(s => {
if (searchInputTitle.length === 0)
return true
else
return s.Title.toLowerCase().match(searchInputTitle.toLowerCase())
}).map(....)
Found the reason. Thanks for all your help.
The issue was the setIsFiltered(true); in the title filter function. Other filters were running based on this line and these other filters were recalling the unfiltered list everytime a key was pressed. I removed this line and the issue was fixed.
I have come to realise that useEffect is almost completely mandatory on most of my projects and is React hooks are quite a departure from the original React syntax.
Hello I was trying to store some data in my array and I did try allData.push(Data1, Data2, Data3) but it said .push was not a function and it also didn't let me just allData(Data1, Data2, Data3), w/e data I get it overrides the previous one. I also tried allData(Data1+Data2+Data3) finally I try something someone suggested me setAllItems([LibrosID1, LibrosID2, LibrosID3]) but it ended up looping and bringing an error:
My "closest" attempt so far has being this one :
useEffect(() => {
let x = 0;
LibrosID1.map((i) => {
x += i.precio
})
LibrosID2.map((i) => {
x += i.precio
})
LibrosID3.map((i) => {
x += i.precio * i.necesita
})
setSubTotal(parseFloat(x).toFixed(2))
setAllItems([LibrosID1, LibrosID2, LibrosID3])
console.log(allItems)
}, [LibrosID1, LibrosID2, LibrosID3]);
What I do not understand is that I'm doing exactly the same with setSubTotal but that doesn't give me any kind of issue and it doesn't make sense that setAllItems give me an error and loop otherwise setSubTotal would loop and error as well right? so what am I doing wrong?
Any tips, documentation and help is welcome.
If you require the whole code let me know but I doubt is required.
Update - Edit:
So I fixed the loop bit and separated the code so people don't get confuse, but now I'm having a delay when I try to get data
useEffect(() => {
setAllItems([LibrosID1, LibrosID2, LibrosID3])
console.log(allItems)
}, [LibrosID1, LibrosID2, LibrosID3]);
When I click the checkbox I get to see how the data is getting parse on LibrosID1 but it says 0 on allItems:
When I click the checkbox again it says 12, even though I just "uncheck" them.
I don't get it! very frustrated atm
You can try the following:
let librosID1 = ["a"];
let librosID2 = ["b"];
let librosID3 = ["c"];
const [allItems, setAllItems] = useState(null);
const [subTotal, setSubTotal] = useState();
useEffect(() => {
setAllItems([...librosID1, ...librosID2, ...librosID3]);
}, []);
console.log(allItems)
The spread operator is used to display or combine all elements from objects or arrays.
References:
Spread syntax (...). https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax. (Accessed 28 September, 2021).
So I have an implementation of a Text Field input alongside a table in which I'm trying to update the state of staged Data before I submit the data to an API.
In the Dialogs parent component, I have the data defined which I want to show in a table as the original state.
The current problem I'm having is the inputted data is somehow updating the original data's state even though I'm not directly touching this data.
Below is a reproduction of it on Codesandbox, So when you open the link typing into the edit value field should not update the current stock field and I don't see why it is.
CodeSandBox
Here is the callback that modifies the state:
const handleUpdateDip = (value, tank) => {
const newData = stagedData;
const foundIndex = newData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
newData.dips[foundIndex].currentStockValue = Number(value);
setStage({
...stagedData,
dips: newData.dips
});
}
};
So yeah this one seems weird to me and I've been banging my head against the keyboard trying to understand whats going on with it since last night so any help would be appreciated!
You are mutating the current object. Try this
setStage((stage) => {
const foundIndex = stage.dips.findIndex((d) => d.tank === tank);
return {
...stage,
dips: stage.dips.map((d, index) => {
if (foundIndex === index) {
return { ...d, currentStockValue: Number(value) };
}
return d;
})
};
});
Instead of this
const foundIndex = stagedData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
stagedData.dips[foundIndex].currentStockValue = Number(value);
setStage({
...stagedData,
dips: stagedData.dips
});
}
I don't see why it's shouldn't update while the code tells it to do so! This line inside handleUpdateDip():
stagedData.dips[foundIndex].currentStockValue = Number(value);
You shouldn't directly mutate the state. You should make a copy of it first change whatever you want and then set the state to the new value e.g.:
const handleUpdateDip = (value, tank) => {
const foundIndex = stagedData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
const newStagedData = { ...stagedData };
newStagedData.dips[foundIndex].currentStockValue = Number(value);
setStage(newStagedData);
}
};
stagedData.dips[foundIndex].currentStockValue = Number(value); this line updates the value of currentStockValue which is used in the "Current Stock" column.
It seems like the table cell left of the input field simply uses the same state that is changed in handleUpdateDip
<TableCell align="right" padding="none">
{row.currentStockValue}
</TableCell>
<TableCell align="right" padding="none">
<InputTextField
id="new-dip"
type="number"
inputProps={{
min: 0,
style: { textAlign: "right" }
}}
defaultValue={row.currentStockValue}
onChange={(event) =>
handleUpdateDip(event.target.value, row.tank)
}
/>
both are currentStockValue, which handleUpdateDips changes in this line
stagedData.dips[foundIndex].currentStockValue = Number(value);
I think I know what you're thinking. You think that on the one hand, you're updating your state in handleUpdateDip(event.target.value, row.tank) with setStage({...}), so you're only changing your state stagedData.
You value for the "Current Stock", however, is mapped to your data variable and not to stagedData.
So in the end your question is: Why ist data changing when you're only manipulating stagedData.
Of course it happens here: const [stagedData, setStage] = useState(() => data);
(btw you don't need to use a function here, const [stagedData, setStage] = useState(data); is fine). You pass in data by reference here, when your setState hits, the reference will be updated and so will your data.
(another BTW: don't call your state variable settings functions simply setState, this is something used by class components in React. Call them like the state you want to set, e.g. setStagedData).
Now, you can elimate this reference, since you only want the initial values anyways. You could do this by passing a copy, like this: const [stagedData, setStagedData] = useState({...data}); But this still won't work - I not really sure why because I don't know enough about the inner workings of useState, but the reason probably is because it's only a shallow copy instead of a deep copy (you can read more about this here).
But if we do a deep copy and pass this in, it works and your original data will stay untouched. You can deep copy by basically stringifying and then parsing it again (which will not copy any methods the object has, just as a warning).
const copy = JSON.parse(JSON.stringify(data));
const [stagedData, setStagedData] = useState(copy);
And just like that your current stock will stay the same:
I forked your CodeSandBox, so you can see it for yourself.