How to update specific object property in React state - reactjs

I'm trying to make my code more concise and therefore removed all of these from separate useState hooks and put them into one. But I'm now trying to update the visible property of the objects based on a toggle state. I've attempted I've looked through different answers about immutable state / mapping through object etc. But it's confusing me even more. What do I put in between my function to change state on toggle change?
const [columnsVisible, setColumnsVisible] = useState({
column1: { visible: true },
column2: { visible: true },
column3: { visible: true },
column4: { visible: true },
});
const changeColumnVisibility = () => {
}
return(
{columnsVisible.column1.visible &&
(
<Column
<>
{toOrderState.map((job, index) => (
<JobCard
job_number={job.jobNumber}
time={job.time}
/>
))}
</>
/>
)}
)

One option to achieve immutability is by using Spread operator. As an example:
const myObject = { a: 1, b: 'some text', c: true };
const myCopy = { ...myObject };
results in myCopy being identical to myObject but changing myCopy (e.g. myCopy.a = 2) will not change myObject.
Now extend that so that myObject has more complex stuff in it instead of just an integer or a boolean.
const myObject = { a: { visible: true, sorted: 'ASC' } };
const myCopy = { ...myObject };
The thing here is that if you change myCopy's visible (for example, myCopy.a.visible = false) you will be changing myObject (myObject.a.visible will also be false even though you haven't explicitly changed myObject).
The reason for the above is that the Spread operator is shallow; so when you are making a copy of a you really are making a copy of the reference to the original a ({ visible: true, sorted: 'DESC' }). But guess what, you can use the Spread operator for copying a.
const myObject = { a: { visible: true, sorted: 'ASC' } };
const myCopy = { a: { ...myObject.a } };
Before addressing specific question, there is one more interesting thing about the Spread operator... you can overwrite some of the copied values simply by adding a key to the new object
const myObject = { a: 1, b: 'some text', c: true };
const myCopy = { ...myObject, a: 2 };
with the above, myCopy.a will be 2 rather than `1.
So now think about what needs to change when changeColumnVisibility is called... you want to change one of the column details but not the others
const [columnsVisible, setColumnsVisible] = useState({
column1: { visible: true },
column2: { visible: true },
column3: { visible: true },
column4: { visible: true },
});
// columnName is either `'column1'`, `'column2'`, ...
const changeColumnVisibility = (columnName) => {
setColumnsVisible((currentColumnsVisible) => {
const isVisible = currentColumnsVisible[columnName].visible;
return {
...currentColumnsVisible,
[columnName]: { ...currentColumnsVisible[columnName], visible: !isVisible }
};
});
}
[columnName] is important to be used so that you actually end up setting column1 or column2 or whatever rather than adding an attribute called columnName.
The final thing to call out from the snippet is that I'm using setColumnsVisible with a callback to ensure that the values are changed correctly rather than using columnsVisible directly.
Hope this helps

Related

Update nested React state?

I'm trying to update part of a state object that is nested. This is the object:
const [buttonObject, setButtonObject] = useState({
intro: [
{
id: '123',
name: 'first_intro_name',
selected: false,
},
{
id: '124',
name: 'second_intro_name',
selected: false,
},
],
experience: [
{
id: '789',
name: 'first_experience_name',
selected: false,
},
{
id: '8910',
name: 'second_experience_name',
selected: false,
},
],
});
When a button is clicked I want to toggle the selected state. I'm using a click handler that looks like this:
const handleButtonClick = ({ id, selected }) => {
if (id === '123') {
buttonsObject.intro.map(
pref => (pref.selected = pref.id === id ? !pref.selected : selected)
);
setButtonsObject(buttonsObject);
} else if (id === '124') {
buttonsObject.intro.map(
pref => (pref.selected = pref.id === id ? !pref.selected : selected)
);
setButtonsObject(buttonsObject);
}
};
It would handle experiences as well. The issue is that right now it seems like rather than updating the object it just overwrites the object or creates a new one. It also doesnt pass that information back down to the component even though I have it routed correctly so it should.
Is there better/correct syntax for updating nested state like this?
Thanks.
instead of checking again with if condition use higher order array function and spread operator to get optimal solution.
setButtonObject(previous => {
return {
...previous,
info: previous.info.map(item => item.id === id ? {
...item,
selected: true
} ? item)
}
})

Finding an array item in state

I have the following array in my Sate:
this.state = {
currentAsset: null,
assets: [
{ id: uuidv4(), selected: false, text: 'Sorte',},
{ id: uuidv4(), selected: false, text: 'optical_dillusion2322.jpg'},
{ id: uuidv4(), selected: false, text: 'man_read_abook3242332.jpg'},
]
}
I want to find one of these assets and assign it to currentAsset. I have the following function:
handleAssetIDChange(assetID) {
console.log(assetID)
var currentAsset = this.state['assets'][assetID]
console.log(currentAsset)
// this.setState({
// currentAsset: this.state['assets'][assetID],
// openAssetSideBar: true
// }, () => {
// console.log(this.state['assets'][assetID])
// })
}
You can see the commented out part is now working. I am trying to set the currentAsset and then trigger the open of the sidebar to display the contents, but currentAsset is not getting set.
I have the assetID, how can I locate the one I need? Another question i have is in many stackoverflow posts they reference state vars like objects, aka:
this.state.assets but I always get an error when trying to do that.
How can I assign currentAsset and then trigger the openAssetSidebar when it has been set?
I would use a filter use get the object, this should update your currentAsset in the state.
handleAssetIDChange(assetID) {
console.log(assetID)
var currentAsset = this.state['assets'].filter( item => item.id === assetID)[0]
console.log(currentAsset)
this.setState(( prev ) => {
...prev,
currentAsset,
openAssetSideBar: true
})
}
In order to find the asset that you are looking for with a specific id, instead of
var currentAsset = this.state['assets'][assetID]
You should do:
const foundAsset = this.state['assets'].find((el) => el.id === assetID)
then to update the state, you would do:
this.setState((prev) => ({...prev, currentAsset: foundAsset}))

Update field in objects array React

I have an objects array like this below:
const [categories, setCategory] = useState([
{ text: "a", active: false },
{ text: "b", active: false },
{ text: "c", active: false },
{ text: "d", active: false }
]);
and I want to update the active fields to be true. I tried something like this but it doesn't work correctly at all:
const clearFilters = () => {
setCategory(categories.map((m) => (m.active = true)));
};
const clearFilters = () => {
setCategory(categories.map((category) => ({...category, active: true})));
};
This must work. ".map" s callback function will be executed for every item of array, and every time will expect from you the new changed item. So you must return the new changed item. In my example above I used ES6 destructuring syntax, if you use ES5 it will be like this.
const clearFilters = () => {
setCategory(categories.map((category) => ({text: category.text, active: true})));
};
And yes, if you return the object in arrow function right away instead of executing function, you must put it into parentheses, otherwise it will understand it as function body, not object
Try something like below:-
const clearFilters = () => {
const clearedCategories = categories.map(m => {
m.active = true;
return m;
});
setCategory([...clearedCategories]);
};

React state comparison with objects and nested arrays

I am trying to compare state against a previous version of the same object (useRef) to detect for changes.
The object that loads into state with useEffect looks like this:
{
active: true,
design: [
{
existing: '55Xgm53McFg1Bzr3qZha',
id: '38fca',
options: ['John Smith', 'Jane Doe'],
required: true,
title: 'Select your name',
type: 'Selection List'
},
{
existing: '55Xgm53McFg1Bzr3qZha',
id: '38fca',
options: ['John Smith', 'Jane Doe'],
required: true,
title: 'Select your name',
type: 'Selection List'
},
],
icon: '🚜',
id: '92yIIZxdFYoWk3FMM0vI',
projects: false,
status: true,
title: 'Prestarts'
}
This is how I load in my app object and define a comparitor state (previousApp) to compare it and detect changes (with lodash) this all works great until I change a value in the design array.
const [app, setApp] = useState(null)
const [edited, setEdited] = useState(false)
const previousApp = useRef(null)
useEffect(() => {
if (app === null) {
getApp(someAppId).then(setApp).catch(window.alert)
}
if (previousApp.current === null && app) {
previousApp.current = { ...app }
}
if (previousApp.current && app) {
_.isEqual(app, previousApp.current) ? setEdited(false) : setEdited(true)
}
}, [app])
For example I change the input value of app.design[0].title = 'testing' with the following code:
const updateItem = e => {
let newApp = { ...app }
let { name, value } = e.target
newApp.design[0][name] = value
setApp(newApp)
}
This works in the sense that it updates my app state object but not that it detects any changes in comparison to previousApp.current
When I console log app and previousApp after changing both values, they are identical. Its seems to update my previousApp value aswell for some reason.
Rearranging the design array works as expected and detects a change with the following function:
const updateDesign = e => {
let newApp = { ...app }
newApp.design = e
setApp(newApp)
}
It probably wasn't detecting any difference because the array present in the design property was keeping the same reference.
When you do this:
let newApp = { ...app }
You're creating a new object for your app, but the property values of that object will remain the same, because it's a shallow copy. Thus, the design array (which is an object, and therefore is handled by reference) will keep its value (the reference) unaltered.
I think this would also solve your problem:
const updateItem = e => {
let newApp = { ...app };
let newDesign = Array.from(app.design); // THIS CREATES A NEW ARRAY REFERENCE FOR THE 'design' PROPERTY
let { name, value } = e.target;
newDesign[0][name] = value;
setApp({
...newApp,
design: newDesign
});
}
I found that using lodash's cloneDeep function i was able to remove the strange link that was occuring between previousApp and App; Even though I used {...app}
See the following:
if (previousApp.current === null && app) {
previousApp.current = cloneDeep(app)
}

SetState not working with spread operator

I have a language switcher in a React native app, it has the selected language ticked, which is stored in state. However this is not working, any advice appreciated. The code is as below. The selectedLanguage is being populated so it knows what needs updating but it's not updating the state object.
constructor(props) {
super(props);
this.state = {
languages: [
{
language: 'Dutch',
selected: false,
},
{
language: 'English',
selected: true,
},
{
language: 'French',
selected: false,
},
{
language: 'German',
selected: false,
},
{
language: 'Polish',
selected: false,
}
],
};
};
changeLanguage(selectedLanguage){
this.state.languages.map((language) => {
if(language.language === selectedLanguage){
this.setState(prevState => ([
...prevState.languages, {
language: selectedLanguage,
selected: true,
}
]));
}
});
}
The spread operator isn’t going to deep compare objects in an array. You’ve got to either move from a languages array to a languages object, as in:
languages: {
Dutch: false,
English: true,
...
}
Or, you need to copy and replace:
changeLanguage(selectedLanguage){
this.state.languages.map((language, index) => {
if(language.language === selectedLanguage){
this.setState(prevState => {
const newLangs = [...prevState.languages];
newLangs[index].selected = true;
return newLangs;
});
}
});
}
First need to find out the right object for given selectedLanguage from an array.
Use Array#map to traverse and update the matched object selected prop value with boolean true and rest to false.
const {languages} = this.state;
const updatedLang = languages.map((lang, key) => {
if (lang.language == selectedLanguage) {
lang.selected = true;
} else {
lang.selected = false;
}
return lang;
})
Now do setState.
this.setState(prevState => ({languages: updatedLang}));

Resources