React State - Unable to update the state - reactjs

Update: Fixed it. It was a schoolboy error. I had actually declared a const BaseObject = () => in the render function, and then I was using return in render function. Sorry for wasting your time guys. I moved the const outside as function, and everything is fine.
I have a very simple state:
this.state = {
filters: [{ key: '', value: '' }, { key: '', value: '' }]
}
And part of render method is:
<div>
{
this.state.filters.map((filter, index) => {
return <div key={'filter_parent_div_'+index} style={{ display: 'flex', alignItems: 'center' }}>
<AutoComplete key={'filter_auto'+index} className='width30 sane-margin-from-right' data={keys} onItemSelected={this.onKeySelected(index)} label='Filter'/>
<div key={'filter_div_'+index} className='sane-margin-from-right width30'>
<FormattedTextbox
id={'filterValue_'+index}
key={'filter_text_'+index}
type='text'
label='Filter Value'
defaultValue={filter.value}
onChange={this.filterTextChanged(index)}>
</FormattedTextbox>
</div>
{ index > 0 ? <div className='show-hand' onClick={this.deleteFilter(index)}><i style={{ color: 'lightgray' }} className='sane-margin-from-top material-icons'>clear</i></div> : '' }
</div>
})
}
</div>
It produces a result something like this:
Now my problem is that when I am trying to add a text in the textfield, it is somehow refreshing the state and it is moving my cursor away every time. For example, in my first row, if I am trying to enter 'Vikas' in Filter Value textfield, As soon as I enter 'V', it refreshes the component and my focus goes away. It looks like I am doing something silly here and somehow messing up the state. My text change method is:
filterTextChanged = index => event => {
const filters = this.state.filters; // or JSON.parse(JSON.stringify(this.state.filters))
filters[index].value = event.target.value;
this.setState({ filters });
};
So I can see that this approach is totally wrong, but I am unable to find the best approach in such case. If I have a single object in my state, I can easily use that state and update the state only for that object, but how to do it with the Array? Any help? I can find workarounds like using onBlur instead but I really need to find the best approach to use arrays in the state. Any help?

The problem is with your key, you are using the index as the key, but it isn't identifiable, i.e it will change when you change your filters, try using a uniquer key for each filter that is persisted with state changes and it should fix your problem.

As far as I can tell you're not keeping your component up to date with the state as you're not setting the value of the component. I might be reading the code wrong, but try adding value={filter.value} instead of defaultValue={filter.value}.
If that doesn't work, a fiddle would be very helpful.

Related

How to solve the problem of a card being bookmarked by page using react, typescript and mui?

When clicking on the card's favorite icon, I need to favorite only one card at a time, however, this does not happen. For example, if I click on the first card on any page, all other first cards are bookmarked. So I have the problem of cards being favorited per page and I want only what I clicked to be favorited.
To solve the problem in App.tsx -> CardAction I put the following code.
<Button size="small">
<FavoriteIcon
onClick={() => changeColorFavorite(id)}
style={{ color: iconFavorite[id] ? "red" : "gray" }}
Favorite
</Button>
I declared a useState by looping through the data(.json file) and setting false as the initial value for each card.
const [iconFavorite, setIconFavorite] = useState(
[...Array(data.length).keys()].map((i) => false)
);
I also declared a function to change the state between true or false
const changeColorFavorite = (id: number) => {
const newIconFavorite = iconFavorite.slice();
newIconFavorite[id] = !iconFavorite[id];
setIconFavorite(newIconFavorite);
};
How to solve this problem? Can someone help me please? Follow the code in codesandbox.
Code codesandbox here
Why it happened
The id passed to changeColorFavorite is actually the index of the array being mapped, hence the same order of card is marked as favorite on every page.
The function you used for the this map:
{_DATA.currentData().map((item, id) =>
It defines id as the index of item in this array. The actual value you are looking for should be item.id.
More about map()
Solution
Instead, pass item.id should get the desired result:
onClick={() => changeColorFavorite(item.id!)}
(I have to add ! to override type check here, but you can refactor the data type later to make sure id is mandatory and is never undefined)
On top of that, the onClick is currently on your FavoriteIcon, but you might want to move it to the Button so that it does not just trigger by clicking on the icon:
<Button size="small" onClick={() => changeColorFavorite(item.id!)}>
<FavoriteIcon style={{ color: iconFavorite[item.id!] ? "red" : "gray" }} />
Favorite
</Button>
Hopefully this achieves the desired result without major changes.
That being said, I would suggest that the implementation of favorites could use some reconsideration in the future, if you need to expand more feature for those cards for example.
Small refactor
It seems that you're defining an array of false like this, and an error message occurs:
const [iconFavorite, setIconFavorite] = useState(
[...Array(data.length).keys()].map((i) => false)
);
Despite that there might be better solution than handling these states as an array of Boolean, at this moment you could refactor the above code into this:
const [iconFavorite, setIconFavorite] = useState([...data].fill(false))
It does the same but cleaner and without error syntax.
More about fill()
Hope this will help!

How to update a boolean value of an element that its inside an array of objects in react using hooks

I have the following array:
const [notifications, setNotifications] = useState([
{
"idNotification": 1,
"message": "Message number 1",
"hideNotification": false,
},
{
"idNotification": 2,
"message": "message number 2",
"hideNotification": false,
}
])
what i want its to loop through this array to show each notification, and if I click on the X button (the X button appear with the prop "toggle") then i want that the notification value "hideNotification" becomes true, and therefore stop showing up on the list
{
notifications?.map((tdata, index) => (
tdata.hideNotification === false ?
<div key={index}>
<Alert
color='primary'
isOpen={!tdata.hideNotification}
toggle={() => {
// I know this doesn´t work
tdata.hideNotification = true;
}}>
{tdata.message}
</Alert>
</div> : null
))
}
I've seen other posts with similar question but with string values. I don't really know how to apply that to a boolean value
Hi. Please try this:
<Alert
color='primary'
isOpen={!tdata.hideNotification}
toggle={() => {
setNotifications((prevEl) => {
let ind = prevEl.findIndex(
(notification) => notification.idNotification === tdata.idNotification
);
notifications[ind].hideNotification = true;
return [...prevEl];
});
}}>
{tdata.message}
</Alert>
When you want a loop on an array and return the element and sometime not, map is not advised.
You should use filter.
You can filter your list on "hideNotification" by doing const
notificationsToDisplay = notifications?.filter((tdata, index) => {
return tdata.hideNotification === false
})
Now you can map on notificationsToDisplay array. Since it contains only the elements having hideNotification property set to true, you'll never see the ones having the property set to false.
Also to set you hideNotification you have to use a state, you can't modify it directly using an assignation. Otherwise your component won't rerender, and you will not see any change.
You shoud use setNotifications()
Since you know the index or even the id of the element you want to modify you just need to find your element in the notifications array using findIndex
and then update the nofications array using setNotifications()
Ok, #Sardor is somewhat-correct in his answer, but he is missing explanation and also some of it is incorrect/can be simplified. I tried to edit, but the edit queue was filled.
First, #Sardor is correct in using the setNotifications setter in the toggle method. You do want to set the state, not the object's property value, like the OP suggests with a comment saying it didn't work. #Sardor uses the findIndex to get the appropriate object in the notifications array, but you can simply use the tdata.idNotification to give you the mapped ID. He also tries to edit the notifications state directly inside the setNotifications method which is incorrect.
The point of having the previous state in the params of the setter method; e.g.
setNotifications((previousNotificationState) => ...)
is to use the previous state (at the time of running the setter) to mutate it knowing the previous state has accurate values.
TLDR; React makes it easy to keep track and edit the state, so don't try to make it hard. You should have a setter edit the state which would cause a re-render, here is an example:
{
notifications?.map((tdata, index) => (
hideNotification === false ?
<div key={index}>
<Alert
color='primary'
isOpen={!hideNotification}
toggle={() =>
setNotifications((prevEl) => Array.from(prevEl)
.map((copyTData) => copyTData.idNotification === tdata.idNotification
? { ...copyTData, hideNotification: true }
: copyTData)
)}>
{message}
</Alert>
</div>
: null
)
)
}
Notice how I:
used Array.from() to copy the previous state and then mutate it
The above use of Array.from() to copy the state was something a well respected colleague once told me was good practice.
Last (personal) note. index may be a unique identifier for the key property, but I'd argue you almost always have something on each iteration, besides the index, that could be unique. In this case the idNotification is unique and can be used as the key instead so the index property doesn't clutter the mappings parameters. Indexes mean nothing in an array when the array changes frequently.

react native Why does my async code hang the rendering?

General :
TL;DR: async code hangs rendering.
I have this component with a Modal and inside the Modal it renders a list of filters the user can choose from. When pressing a filter the color of the item changes and it adds a simple code(Number) to an array. The problem is that the rendering of the color change hangs until the logic that adds the code to the array finishes.
I don't understand why adding a number to an array takes between a sec and two.
I don't understand why the rendering hangs until the entire logic behind is done.
Notes: I come from a Vue background and this is the first project where I'm using react/react-native. So if I'm doing something wrong it would be much appreciated if someone points that out
Snack that replicates the issue :
Snack Link
My code for reference :
I use react-native with expo managed and I use some native-base components for the UI.
I can't share the whole code source but here are the pieces of logic that contribute to the problem :
Parent : FilterModal.js
The rendering part :
...
<Modal
// style={styles.container}
visible={modalVisible}
animationType="slide"
transparent={false}
onRequestClose={() => {
this.setModalVisible(!modalVisible);
}}
>
<Center>
<Pressable
onPress={() => this.setModalVisible(!modalVisible)}
>
<Icon size="8" as={MaterialCommunityIcons} name="window-close" color="danger.500" />
</Pressable>
</Center>
// I use sectionList because the list of filters is big and takes time to render on the screen
<SectionList
style={styles.container}
sections={[
{ title: "job types", data: job_types },
{ title: "job experience", data: job_experience },
{ title: "education", data: job_formation },
{ title: "sector", data: job_secteur }
]}
keyExtractor={(item) => item.id}
renderItem={({ item, section }) => <BaseBadge
key={item.id}
pressed={this.isPressed(section.title, item.id)}
item={item.name}
code={item.id}
type={section.title}
add={this.addToFilters.bind(this)}
></BaseBadge>}
renderSectionHeader={({ section: { title } }) => (
<Heading color="darkBlue.400">{title}</Heading>
)}
/>
</Modal>
...
The logic part :
...
async addToFilters(type, code) {
switch (type) {
case "job types":
this.addToTypesSelection(code);
break;
case "job experience":
this.addToExperienceSelection(code);
break;
case "formation":
this.addToFormationSelection(code);
break;
case "sector":
this.addToSectorSelection(code);
break;
default:
//TODO
break;
}
}
...
// the add to selection methods look something like this :
async addToTypesSelection(code) {
if (this.state.jobTypesSelection.includes(code)) {
this.setState({ jobTypesSelection: this.state.jobTypesSelection.filter((item) => item != code) })
}
else {
this.setState({ jobTypesSelection: [...this.state.jobTypesSelection, code] })
}
}
...
Child :
The rendering Part
render() {
const { pressed } = this.state;
return (
< Pressable
// This is the source of the problem and read further to know why I used the setTimeout
onPress={async () => {
this.setState({ pressed: !this.state.pressed });
setTimeout(() => {
this.props.add(this.props.type, this.props.code);
});
}}
>
<Badge
bg={pressed ? "primary.300" : "coolGray.200"}
rounded="md"
>
<Text fontSize="md">
{this.props.item}
</Text>
</Badge>
</Pressable >
);
};
Expected outcome :
The setState({pressed:!this.state.pressed}) finishes the rendering of the item happens instantly, the rest of the code happens after and doesn't hang the rendering.
The change in the parent state using the add code to array can happen in the background but I need the filter item ui to change instantly.
Things I tried :
Async methods
I tried making the methods async and not await them so they can happen asynchronously. that didn't change anything and seems like react native ignores that the methods are async. It hangs until everything is done all the way to the method changing the parent state.
Implementing "event emit-listen logic"
This is the first app where I chose to use react/react-native, coming from Vue I got the idea of emitting an event from the child and listening to it on the parent and execute the logic that adds the code to the array.
This didn't change anything, I used eventemitter3 and react-native-event-listeners
Using Timeout
This is the last desperate thing I tried which made the app useable for now until I figure out what am I doing wrong.
basically I add a Timeout after I change the state of the filter component like so :
...
< Pressable
onPress={async () => {
// change the state this changes the color of the item ↓
this.setState({ pressed: !this.state.pressed });
// this is the desperate code to make the logic not hang the rendering ↓
setTimeout(() => {
this.props.add(this.props.type, this.props.code);
});
}}
>
...
Thanks for reading, helpful answers and links to the docs and other articles that can help me understand better are much appreciated.
Again I'm new to react/react-native so please if there is some concept I'm not understanding right point me in the right direction.
For anyone reading this I finally figured out what was the problem and was able to solve the issue for me.
The reason the rendering was getting hang is because the code that pushes to my array took time regardless of me making it async or not it was being executed on the main thread AND that change was triggering screen re-render which needed to wait for the js logic to finish.
The things that contribute to the solution and improvement are :
Make the array (now a map{}) that holds the selected filters stateless, in other words don't use useState to declare the array, instead use good old js which will not trigger any screen re-render. When the user applies the filters then push that plain js object to a state or context like I'm doing and consume it, doing it this way makes sure that the user can spam selecting and deselecting the filters without hanging the interactions.
first thing which is just a better way of doing what I needed is to make the array a map, this doesn't solve the rerender issue.

Im trying to build a menu that has collapsible options, but the dom does not update even when state data updates

const [arrayOfQuestions, setArrayOfQuestions] = useState([
{
q: 'Are your products safe to use?',
a: 'Yes.',
hidden: true
},
{
q: 'Are your products safe to use?',
a: 'Yes.',
hidden: true
},
{
q: 'Are your products safe to use?',
a: 'Yes.',
hidden: true
},
{
q: 'Are your products safe to use?',
a: 'Yes.',
hidden: true
},
])
const toggleItemOpenAndClose = (e) => {
let array = arrayOfQuestions
array[e.target.id].hidden = !array[e.target.id].hidden
setArrayOfQuestions(array)
}
return (
<div>
<Layout
bgImage={metaData.heroImage.childImageSharp.fluid}
header='Frequently Asked Questions'>
<div className='page-container'>
<div className="content-container">
{
arrayOfQuestions.map((question,i) => {
return (
<div id={i} key={`id${i}`} onClick={toggleItemOpenAndClose} className='block-container'>
<div id={i} className='white smallerheader'>
{question.q}
</div>
{
question.hidden ?
null :
<div id={i} className='white paragraph'>
<br/>
{question.a}
</div>
}
</div>
)
})
}
</div>
</div>
</Layout>
</div>
)
}
Im using Gatsby and react hooks.
Im trying to build a collapsible menu (an faq) sort of like a menu, so that when you click on one of the options, it expands and shows you the answer associated with the question. However, I'm having trouble making it work in real time. whenever i click on the option, my data updates, but the dom itself doesnt update when i click on the option. then, when i change something in the code, the app updates (hot reloader?) and then the dom updates. As far as i understand it, when i change state in real time, the dom should also update in real time, but I can't understand why its not in this case. Has anyone experienced something like this before?
You should not be updating state directly. This should be your toggle code
const toggleItemOpenAndClose = (e) => {
// This will create a new array and with all new objects to preserve immutability
let newArray = arrayOfQuestions.map(x => {
return {... x}
}
newArray[e.target.id].hidden = !newArray[e.target.id].hidden
setArrayOfQuestions(newArray)
}
Make a copy arrayOfQuestions like so,
let array = [...arrayOfQuestions]
Why ?
What you're updating is an object property but it's still belongs to the same array ref. When you spread over the array, you will unpack the object references in your array and pack them in a new array due to those [] and then setting this new array will actually trigger the render.
In case you want to make copy of the objects as well in your array, use map as suggested by #Doug Hill which I think looks better.
But in your case simply spreading will also work for now. Things get messy when there are nesting depths of objects and then consider using libraries which supply you with deep cloning utilities.

Updating a value that's within an array within an array in a state

I'm trying to update the state of the id value in shoeList. Currently I have a textfield that allows for entering of a new ID and I want the state to update when the OK button is clicked.
Here is some of the relevant code:
state = {
editingToggle: false,
shoeList : [
{name: 'bob', id: '123213-0', shoeSize: 'L'}
],
}
<TextInput className='text-area-header' id="textM" width="m" type="text" placeholder={this.state.shoeList[0].id} />
<Button className="ok-button" variant="tertiary" size ='xs' type="button" handleClick={this.saveHeader}>OK</Button>
saveHeader(e) {
this.setState(state=> ({
shoeList[0].name:
}))
alert('Header changed to ' + this.state.shoeList[0].id);
e.preventDefault();
}
I'm not sure what to put in this.setState as I haven't found anything on how to update nested values through a google search. Also whenever I put a value attribute to the TextInput tags it doesn't allow me to edit in the textinput on the webpage anymore. Any help would be great
Consider this example:
saveHeader() {
this.state.shoeList[0].id = 'newid'
this.setState({ shoeList: this.state.shoeList })
}
setState() checks if the values have changed, and if not, it does not update the component. Changing a nested value does not change the reference to the array, which means that simply calling setState() is not enough.
There are two ways around this. The first is to use forceUpdate():
saveHeader() {
this.state.shoeList[0].id = 'newid'
this.forceUpdate()
}
This forces the component to re-render, even though the state didn't change.
The second way is to actually change the shoeList array by creating a new one:
saveHeader() {
let newShoeList = this.state.shoeList.slice()
newShoeList[0].id = 'newid'
this.setState({ shoeList: newShoeList })
}
Using .slice() on an array creates a new array that is completely identical. But because it is a new array, React will notice that the state changed and re-render the component.

Resources