Updating deeply nested state with useState not working properly - javascript

I followed the answer in this thread to try to update my deeply nested object in React.
React: Setting State for Deeply Nested Objects w/ Hooks
What seems to work like a charm there, will somehow break for me when doing the following:
I have a table populated with items from an array defined like so:
const [items, setItems] = useState([
{
selected: false,
title: 'Item 1',
status: 'new'
},
{
selected: false,
title: 'Item 2',
status: 'used'
},
]);
When selecting an item from that list this function gets called to update selected variable for the object with the index i like so:
const select = (e) => {
const i = e.target.getAttribute('data-index');
setItems((prevState) => {
prevState[i].selected = !prevState[i].selected;
return [...prevState];
});
};
This will work exactly once. If I trigger select a second time or any time after that return [...prevState] somehow keeps returning the state unchanged. (selected stays true forever). I can't solve this.
items is attached to a component List like so:
<List
items={items}
/>
and inside List (shortened code):
{items.map((item, i) => {
return (
<tr className="list-table-tr">
{hasSelector ? (
<td className="list-table-td-selector">
{item.selected ? (
<div
data-index={i}
className="global-selector-selected"
onClick={select}
></div>
) : (
<div
data-index={i}
className="global-selector-unselected"
onClick={select}
></div>
)}
</td>
) : null}

You're breaking one of the primary rules of React state: You're modifying a state object directly, rather than making a copy.
To correctly do the update, you'd do this:
const select = (e) => {
const i = e.target.getAttribute('data-index');
setItems((prevState) => {
// Copy the array (your code was doing that)
const update = [...prevState];
const item = update[i];
// Copy the object (your code wasn't doing that) and update its
// `selected` property
update[i] = {...item, selected: !item.selected};
return update;
});
};
Note how both the array and the object are copied, rather than just the array.

Related

React | Adding and deleting object in React Hooks (useState)

How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};

React state is updating but the component is not

There is a component that maps through an array stored in the state. A button, when it is clicked it updates the state, this action is working.
The problem is that the component is not updating too.
Here is the code:
const MyComponent = () => {
...
const [fields, setFields] = useState([{value: 'test', editable: false},
{value: 'test2', editable: false}]);
...
const toggleClass = (id) => {
const aux = fields;
aux[id].editable = true;
setFields(aux);
}
...
return (
<div>
...
{fields.map((field, id) => {
return (
<div>
<input className={field.editable ? 'class1' : 'class2'} />
<button onClick={() => toggleClass(id)}>click</button>
</div>
);
})}
</div>
);
I put logs and the state (fields) is updated after click to editable = true. But the css class is not changing.
Is there any solution to this issue?
You need to make a copy of your existing state array, otherwise you're mutating state which is a bad practice.
const toggleClass = id => {
const aux = [...fields]; //here we spread in order to take a copy
aux[id].editable = true; //mutate the copy
setFields(aux); //set the copy as the new state
};
That's happening because you are mutating the value of fields, which makes it unsure for React to decide whether to update the component or not. Ideally if you should be providing a new object to the setFields.
So, your toggleClass function should look like something below:
const toggleClass = (id) => {
const aux = [...fields]; //This gives a new array as a copy of fields state
aux[id].editable = !aux[id].editable;
setFields(aux);
}
BTW, I also noticed that you're not assigning a key prop to each div of the the map output. Its a good practice to provide key prop, and ideally keep away from using the index as the key.

Dynamic className in map not changing after update

I'm trying to update my react className when the active changes in the sites variable which is mapped to loop through the items.
What happens is that the className 'inactive' does not go away if the active status changes to true or visa versa.
Code:
// Context: this code is inside of the component
const [sites, setSites] = useState([]); <--- Updated dynamically with fetch()
const changeActive = (id) => {
const tmpSites = sites;
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
return (
{sites.length ? sites.map((item, i) => {
return (
<tr className={`${!Boolean(item.active) ? 'inactive' : ''}`} key={item.id}>
// inbetween data
</tr>
)
}) : null}
)
You need to create a copy of the sites array and make changes to the copy and then set it in state. Never mutate state directly as it might not cause a re-render as we are updating the state with the same object reference.
const changeActive = (id) => {
const tmpSites = [...sites];
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
Because you are mutating the original sites Object and not cloning it before making the changes, the useState ("setSites") does not actually re-renders the component because it cannot compare previous Object to current, because they are the same.
You must do a deep-clone of the sites Array of Objects:
const changeActive = (id) => {
setSites(sites => {
sites.map(site => ({ // ← first-level clone
...site // ← second-level clone
active: site.id === id ? !site.active : site.active
}))
})
}
It is imperative to use the setSites function that returns the current state and then you can reliably deep-clone it.

React - Trying to remove child component, but there is some issue with the function I pass

I am trying to remove a child component by sending a function and an id and then calling that function
when a button in the child is clicked.
Note: The child is a class component, the parent a functional component
Here is the function defined in the parent:
const removeTable = (tableId) => {
const newArray = tables.filter((el) => (el.id !== tableId)
);
console.log(newArray);
setTables(newArray)
}
This is removing elements, but not the one I want. Instead of removing the element with the id I pass it, it keeps that number, starting from 0.
So when I click on the item with a id of 3, it only keeps 0-2. In this example it should keep 0-2 and 4-6. (The array is supposed to be 7 elements long, but somehow it is shortened (before the filter))
What I've tried and Discovered:
I was completely lost, so I decided to create a mock function without using the child:
const removeTabletest = () => {
const key = 1;
const testArr = [{id:1,op:"adsad"}, {id:2,op:"adsad"},{id:3,op:"adsad"} ];
const fml = testArr.filter( (el) => (el.id !== key));
console.log(fml)
}
This function works as I expect.
Finally I stumbled on the fact that when I console.log(tables) at the beginning of the function, I am not getting the same data as in React Dev Tools. The tables array is not the full array I expect.
But when I create another button that is called by the parent (instead of the child), then tables is logged correctly:
Any idea what is going on? or how else I can achieve this?
This sounds like a difficult approach to use with React. Generally what you want to do is just filter your array in the render method based on props or state. If you need a child element to modify what you filter by, like a button, you would pass a callback to the child. Here is an example:
class Table extends React.Component {
constructor(props) {
super(props);
this.state = { hiddenItems: [] };
}
hideItem = item => {
// to hide a row of the table we add it to the list
// of hidden items.
this.setState({ hiddenItems: [...this.state.hiddenItems, item] });
}
render() {
// Create the table element by filtering out hidden items
const table = this.props.items
.filter(item => !this.state.hiddenItems.includes(item))
.map(item => {
return (
<div key={item.id}>{item.contents}</div>
);
});
return (
<>
{table}
// the onClick function could also be passed to a
// child React object
<button onClick={() => this.hideItem(this.props.item[0])}>
Hide item 0
</button>
</>
);
}
}
Where this.props.items would look something like:
[{ id: 0, contents: 'blah blah'},
{ id: 1, contents: (<span>blah</span>)}]
Of course you can also have a function unhiding an item but hopefully this shows the general approach.

Mapping over array of objects return nothing but array is clearly filled, using React and context to pass state

Scenario::
I have a parent component () that is using context API this is the Provider that passes the state to children components. Each of these children components has children ( a form to handle input ) when those forms are submitted I then use props to pass the input all the back up the parent component () aka the Provider whose job is to call an external script to calculate data from what the user submits which is received as a promise. Once I get the data I setState with the updated data.
My Problem::
I have two arrays of objects that are added to the state after the user submits the form. One containing all the values of the input the user typed in. Two the results the external script returned based on however many values the user typed in. Now is when I want to render both arrays to the view once we get them. Array one renders fine, but array two renders nothing. When I log array two in the console the array is filled, but if I log the array[index] and give it a specific index I get undefined
1st User submits the form
<KeywordSearch submitForm={this.handleKeywordFormSubmit} name="Bracket" />
Form is passed up to the Parent Component the Provider it looks like this
handleKeywordFormSubmit = () => {
let query = {
options: {
keyword: true
},
data: {
keyword_bracket: this.state.values
}
}
this.props.updateReport(query)
}
Values array is structured like this
values: [
{
value: 'input1',
label: 'input1'
},
{
value: 'input2',
label: 'input2'
}
]
2nd the parent component takes the query and runs the external script, returns the result which is an object which is then pushed in an array. When we're done the new array is added to the Context.Provider state to be accessed by the children components Context.Consumer
handleReportUpdate = (query) => {
if(query.options.keyword){
console.log('Keyword');
let keyword_bracket = []
query.data.keyword_bracket.forEach(bracket => {
tableBuilder.keywords(
this.state.thisReport.client.data.organic_research,
bracket
)
.then(keyword_summary => {
keyword_bracket.push(keyword_summary)
})
})
console.log(keyword_bracket)
let updatedReport = {
client: {
data: {
...this.state.thisReport.client.data,
keywordBrackets: keyword_bracket,
},
info: {
...this.state.thisReport.client.info,
Keyword: query.data.keyword_bracket
}
},
competitors: [...this.state.thisReport.competitors],
name: this.state.thisReport.name,
_id: this.state.thisReport._id
}
this.setState({
thisReport: updatedReport
})
}
}
3rd the Rendering stage in the child component the Context.Consumer
<Grid item xs={12} style={{margin: '20px 0'}}>
{
context.thisReport.client.data.keywordBrackets.length !== 0 ?
context.thisReport.client.data.keywordBrackets.map(bracket =>
{
return(
<div key={bracket.searchTerms.value}>{bracket.searchTerms.value}</div>
)
}
)
:
(<div>Nothing Here</div>)
}
</Grid>
<Grid item xs={12} style={{margin: '20px 0'}}>
<KeywordSearch submitForm={this.handleCompetitorFormSubmit}
name='Competitor' />
</Grid>
<Grid item xs={12} style={{margin: '20px 0'}}>
{
context.thisReport.client.info.Keyword.length !== 0 ?
context.thisReport.client.info.Keyword.map(bracket =>
{
return(
<div key={bracket.value}>{bracket.value}</div>
)
}
)
:
undefined
}
</Grid>
Here's where it's confusing because by following the process above when its time to render the new state from Context the codes second rendering that maps the context.thisReport.client.info.Keyword are rendered perfectly fine on the screen. The first rendering context.thisReport.client.data.keywordBrackets returns nothing. As a test, you can see I have added a <div>Nothing Here</div>
if the condition returns false. At first, before the user goes through the process of submitting the form that is shown on the screen as expected. Once they submit the form it disappears and where the return(
<div key={bracket.searchTerms.value}>{bracket.searchTerms.value}</div>) the output should be shown its blank. I have log's in the console that says the state is there the react dev tools confirms it as well. One weird thing is if I try to access the array by index I get undefined console.log(context.thisReport.client.data.keywordBrackets[0]) //undefined
This is a lot to take in so thanks in advance for reading. If you have any solutions advice lmk!!
Can you try the following?
handleReportUpdate = (query) => {
//create array of jobs
var getKeywordSummaries = [];
if(query.options.keyword){
console.log('Keyword');
let keyword_bracket = []
query.data.keyword_bracket.forEach(bracket => {
let getKeywordSummary = tableBuilder.keywords(
this.state.thisReport.client.data.organic_research,
bracket
)
.then(keyword_summary => {
keyword_bracket.push(keyword_summary)
})
//push each job into the jobs array
getKeywordSummaries.push(getKeywordSummary);
});
//wait until all jobs are done
Promise.all(getKeywordSummaries).then(function(results) {
console.log(keyword_bracket)
let updatedReport = {
client: {
data: {
...this.state.thisReport.client.data,
keywordBrackets: keyword_bracket,
},
info: {
...this.state.thisReport.client.info,
Keyword: query.data.keyword_bracket
}
},
competitors: [...this.state.thisReport.competitors],
name: this.state.thisReport.name,
_id: this.state.thisReport._id
}
this.setState({
thisReport: updatedReport
});
}
}
}

Categories

Resources