updating a property in an array of object immutably using react hooks - javascript

I'm trying to update an array property in an array of objects using react hooks, as we all know when we need to update the state based on a previous state we use the function set"MypopertyName"
as shown below:
My code:
const [MenuList, setMenuList] = useState([
{
id: 0,
title: "sushi",
desc: "finest fish and vegies",
price: "$22.99",
amount: 1,
},
{
id: 1,
title: "shneitzel",
desc: "a german specialty",
price: "$50.99",
amount: 1,
},
{
id: 2,
title: "pizza",
desc: "an italian specialty",
price: "$100.99",
amount: 1,
},
{
id: 3,
title: "pasta",
desc: "an italian specialty",
price: "$200.99",
amount: 1,
},
]);
my problem that I wanna change the amount of the meal based on clicks of the user so here is what I tried:
const onClickHandler = (id) => {
for (const key in MenuList) {
if (MenuList.id === id) {
return (MenuList[key].amount = MenuList[key].amount + 1);
}
}
};
I know that the reactivity system of react only works when I call setMenuList() I tried that but it doesn't work besides I wanna update the specific object immutably so I need to use the function syntax like this:
setMenuList((prevMenuList)=>{
//my code
})
also, I didn't manage to get it work, sorry guys I'm one of the old gurus that used the class-based components for a long time you know with the state={} and so on the whole new hooks concept is new for me.
So any help would be appreciated and thank you in advance.

The use of the function-based setter
setMenuList((prevMenuList)=>{
is useful when the function may be called when the menuList currently in its scope is not what's currently being rendered - for example, for when the function is called after an API call finishes. This is completely different from React's need to not mutate state, which requires another approach entirely.
Map the state array, and when the ID matches, return a new object with the amount incremented.
const onClickHandler = (id) => {
setMenuList(
menuList.map(
obj => obj.id !== id ? obj : ({
...obj,
amount: obj.amount + 1
})
)
);
};

Related

Find item in array of strings to filter a flatlist

I have a FlatList that I am trying to filter with multiple inputs from dropdown selectors. Here is my FlatList component
<FlatList
style={styles.list}
data={data.filter(filteredUsers)}
renderItem={renderItem}
keyExtractor={(item) => item.id}
ListEmptyComponent={EmptyItem}
/>
A sample item from the data object
{ id: "2", name: "Harry", city: "Scottdale", state: "AZ", instrument: ["Cowbell", "Ocarina"] },
And the filterUsers function that is working with a single string, which shows all items in the data object and filters down when a user starts selecting options from the dropdown. When cleared the full data list is shown again.
const filteredUsers = (item: { state: string; instrument: string }) => {
return (
item.state.toLowerCase().indexOf(state.toLowerCase()) >= 0 &&
item.instrument.toLowerCase().indexOf(instrument.toLowerCase()) >= 0
)
}
I'd like to have that same functionality after changing instrument to an array of strings rather than a single string.
Ive updated the instrument type in filteredUsers to instrument: Array<string> but can not figure out how to get the filtering to work as it did before I updated to the array. I appreciate any help and would happily provide more information if I left anything out. Thanks
This may be one implementation to achieve the desired objective:
Sample data:
const data = [
{ id: "2", name: "Harry", city: "Scottdale", state: "AZ", instrument: ["Cowbell", "Ocarina"] },
{ id: "2", name: "Harry", city: "Scottsdale", state: "AZ", instrument: ["Towbell", "Ocarina"] },
{ id: "2", name: "Harry", city: "Scottdale", state: "PA", instrument: ["Towbell", "Odarina"] },
];
Simulate the values that will be used to filter the data:
const state = 'AZ';
const instrument = 'Cowbell';
The filteredUsers method:
const filteredUsers = (item: any) => {
return (
item.state.toLowerCase().indexOf(state.toLowerCase()) >= 0 &&
item.instrument.join().toLowerCase().indexOf(instrument.toLowerCase()) >= 0
)
}
Invoke the filter manually:
console.log(data.filter(filteredUsers));
Link to TypeScript Playground
Explanation
The parameter passed into the filteredUsers method is set to be any (for brevity). It may be appropriate to declare the object's props instead of any.
The values that will be used to filter are state and instrument and those are also declared separately (this may probably come from either state or props)
When searching for state the existing logic applies
When searching for instrument, the newly-transformed Array in the item is turned into a string (by using .join) and existing logic is applied to this new string (which is a concatenation of all elements of the instrument array).
NOTE: This is most-certainly not the best solution. And, I'm sure there are several points that need to be updated. Happy to learn more and update this.

how to alterate state in array object at v-for loop?

im making a inventory system at Vue Js using Nuxt. im trying to make a function that find in the invendory and if the item exist in the inventory the quantity increase in one. The problem? the function is runs successfully but i can not see changes in my V-for list but if im push other object in my array the v-for loop is updated whit the new data.
<script lang="js">
import Vue from 'vue'
import Navbar from '../../components/Navbar'
export default Vue.extend({
components: {
Navbar
},
data(){
return{
products: [
{ name: 'abrazadera', id: 0, image: "https://carritoferretero.com/wp-content/uploads/2018/05/products-abrazadera-de-arranque-carrito-ferretero_2.png", price: 20},
{ name: 'tuerca', id: 1, image: "https://cdn.homedepot.com.mx/productos/819576/819576-d.jpg", price: 40},
{ name: 'martillo', id: 2, image: "https://cdn.homedepot.com.mx/productos/680442/680442-d.jpg", price: 50}
],
venta: [
]
}
},
methods: {
addProduct(id, index){
let busqueda = this.venta.find( item => item.id == id)
console.log(busqueda)
if(typeof(busqueda) == 'undefined'){
let newObj = this.products[index]
newObj.cantidad = 1
this.venta.push(newObj)
} else {
busqueda = this.venta.findIndex( element => element.id == id )
let newObj = this.venta[busqueda]
newObj.cantidad = this.venta[busqueda].cantidad + 1
this.venta[busqueda] = newObj
}
}
}
})
</script>
in my "addProduct" function im find a product in my "venta" inventory if item does not exist im add a product in my inventory else im add + 1 quantity. the function is working correctly but the rendering vfor does not is updating. The v-for list is updating only if im add a new element using "arrayelement.push"
any idea for solve this? thanks for the answers
Vue 2 can't detect changes to existing items in an array when made the usual way; here's the full explanation. You'll need to change this line:
this.venta[busqueda] = newObj
to:
this.venta.splice(busqueda, 1, newObj)
(You can also use Vue.set, but splice is at least still a standard array operation.)

Redux managing arrays of objects and finding nested objects

I'm not sure what to name this, but basically I'm new to React and Redux and looking for a more correct/cleaner way to do this or just how to do this with my current set up.
I have a state that looks like this
--Character
---id
---name
---race
----id
----raceName
----traits
-----trait
------id
------name
------description
-----trait
------id
------name
------description
---classes
----class
-----id
-----className
-----classLevel
-----traits
------trait
-------id
-------name
-------description
------trait
-------id
-------name
-------description
----class
-----id
-----className
-----classLevel
-----traits
------trait
-------id
-------name
-------description
------trait
-------id
-------name
-------description
---traits
----trait
-----id
-----name
-----description
----trait
-----id
-----name
-----description
As you can see(hopefully) traits is an array of object TRAIT and classes is an array of object CLASS, in the end the whole state is quite a messy deal. I've read that I can somehow reference them by ID's but I'm not sure how if IDs are autogenerated.
So I kind of have two questions:
How do I simplify/flatten this structure if it even could be done?
If I can't simplify this structure is there anyway I can find a specific Trait with a specific ID without looping through all the objects that have property traits?
Yes. You can find Trait with a specific ID easily. Let know if this is what you are asking.
// Search in traits directly under Character.
const traitForId = this.state.Character.traits.find((trait) => {
return trait.id = "<SPECIFIC_ID>"
})
// Search in the list of traits under each Class.
const classTraits = this.state.Character.classes.map((class) => class.traits).flat();
const classTraitsForId = classTraits.find((trait) => {
return trait.id = "<SPECIFIC_ID>"
})
Find below recursive way to find a Trait irrespective of where it's present in the state.
function findTraitForId(state, specific_id){
if(state.traits){
const traitForId = state.traits.find((trait) => {
return trait.id == specific_id
});
if(traitForId)
return traitForId;
}
return Object.keys(state).filter((key) => key != 'traits').map((stateItem) => {
return findTraitForId(state[stateItem], specific_id);
}).flat();
}
Tried above function for the input
findTraitForId({'classes':[{traits: [{id: 1, name: "A"}, {id: 2, name: "AB"}]}, {traits: [{id: 3, name: "ABC"}, {id: 4, name: "ABCD"}]}], traits: [{id: 5, name: "ABCDE"}, {id: 6, name: "ABCDEF"}]}, 3)
which return
[{id: 3, name: "ABC"}]

Object.assign does not update my state (React)

I have an app which has a few users. I would now like to be able to create a new user. So I have created this actionCreator:
export const createUser = (first, last) => {
console.log("You are about to create user: XX ");
return {
type: 'USER_CREATE',
first: first,
last: last,
payload: null
}
};
I am dealing only with first & last names for now. The actionCreator gets its parameters from the container. There is a button which calls the actionCreator like so:
<button onClick={() =>this.props.createUser(this.state.inputTextFirstName, this.state.inputTextLastName)}>Submit</button>
My UserReducer looks like this:
/*
* The users reducer will always return an array of users no matter what
* You need to return something, so if there are no users then just return an empty array
* */
export default function (state = null, action) {
if(state==null)
{
state = [
{
id: 1,
first: "Bucky",
last: "Roberts",
age: 71,
description: "Bucky is a React developer and YouTuber",
thumbnail: "http://i.imgur.com/7yUvePI.jpg"
},
{
id: 2,
first: "Joby",
last: "Wasilenko",
age: 27,
description: "Joby loves the Packers, cheese, and turtles.",
thumbnail: "http://i.imgur.com/52xRlm8.png"
},
{
id: 3,
first: "Madison",
last: "Williams",
age: 24,
description: "Madi likes her dog but it is really annoying.",
thumbnail: "http://i.imgur.com/4EMtxHB.png"
}
]
}
switch (action.type) {
case 'USER_DELETED':
return state.filter(user => user.id !== action.userIdToDelete);
case 'USER_CREATE':
console.log("Action first:" + action.first);
console.log("Action last:" + action.last);
Object.assign({}, state, {
id: 4,
first: action.first,
last: action.last,
age: 24,
description: "Some new Text",
thumbnail: "http://i.imgur.com/4EMtxHB.png"
});
return state;
}
return state;
}
Now I have a few questions.
1) Is this the proper way to do this, or am I writing bad code somewhere? Keep in mind that I am trying to use Redux here, I am not entirely sure though whether I am not sometimes falling back into React without Redux
2) Am I doing the state thing correctly? I initially used a tutorial and am now building upon that, but I am not sure why state seems to be an array:
state = [ <--- Does this mean that my state is an array?
{
id: 1,
// and so on ...
I am very confused by this, since in other Tutorials state is just an object containing other smaller objects and its all done with parentheses { }
3) What would be the best way to create a new user. My Object.assign does not work, it does not update anything, and I am not sure where the mistake lies.
4) And, relatedly, how could I update one individual user or a property of one individual user?
As Flyer53 states you need to set the state to the return value of Object.assign() as this is designed to not mutate state it will not change the value of the state you're passing in.
The code's fine; I'd tend to use just one property on the action in addition to its type, so have a property of (say) user that is an object containing all the user data (first name, last name etc).
I believe it's quite idiomatic to define a default state outside of the reducer and then set this as the default value for the state parameter in the reducer function:
export default function (state = initialState, action) {
For a brilliant introduction by its creator, see https://egghead.io/courses/getting-started-with-redux
State can be any shape you like. As an application grows in complexity it will usually be represented as an object composed of different sections of data. So, for example, in your case in could be comprised of an array of users and, say, an 'order by' that could apply to some UI state):
{ users: [], orderBy: 'lastName' }
If you carry on using an array of users as the state then you can use the ES6 spread operator to append the new user, for example:
newState = [ ...state, action.user ];
whereas if you move to using an object for state, the following would similarly append a user:
newState = Object.assign({}, state, { users: [ ...state.users, action.user ] };
Finally, to update a single user you could just use map against the array of users as follows (this is obviously hardcoded, but you could match, say, on id and update the appropriate properties).
let modifiedUsers = state.users.map((user) => {
if (user.id === 3) {
user.name = user.name + '*';
}
return user;
});
let newState = Object.assign({}, state, { users: modifiedUsers });
There's maybe an easier way to log the state(users) in an object (not in an array as in your example code above) that works without Object.assign() which is supposed to work with objects, not arrays:
var state = {
user1: {
id: 1,
first: "Bucky",
last: "Roberts",
age: 71,
description: "Bucky is a React developer and YouTuber",
thumbnail: "http://i.imgur.com/7yUvePI.jpg"
}
};
state['user' + 2] = {
id: 2,
first: "Joby",
last: "Wasilenko",
age: 27,
description: "Joby loves the Packers, cheese, and turtles.",
thumbnail: "http://i.imgur.com/52xRlm8.png"
};
console.log(state);
console.log(state.user2);
Just an idea ...

How to update element inside List with ImmutableJS?

Here is what official docs said
updateIn(keyPath: Array<any>, updater: (value: any) => any): List<T>
updateIn(keyPath: Array<any>, notSetValue: any, updater: (value: any) => any): List<T>
updateIn(keyPath: Iterable<any, any>, updater: (value: any) => any): List<T>
updateIn(keyPath: Iterable<any, any>, notSetValue: any, updater: (value: any) => any): List<T>
There is no way normal web developer (not functional programmer) would understand that!
I have pretty simple (for non-functional approach) case.
var arr = [];
arr.push({id: 1, name: "first", count: 2});
arr.push({id: 2, name: "second", count: 1});
arr.push({id: 3, name: "third", count: 2});
arr.push({id: 4, name: "fourth", count: 1});
var list = Immutable.List.of(arr);
How can I update list where element with name third have its count set to 4?
The most appropriate case is to use both findIndex and update methods.
list = list.update(
list.findIndex(function(item) {
return item.get("name") === "third";
}), function(item) {
return item.set("count", 4);
}
);
P.S. It's not always possible to use Maps. E.g. if names are not unique and I want to update all items with the same names.
With .setIn() you can do the same:
let obj = fromJS({
elem: [
{id: 1, name: "first", count: 2},
{id: 2, name: "second", count: 1},
{id: 3, name: "third", count: 2},
{id: 4, name: "fourth", count: 1}
]
});
obj = obj.setIn(['elem', 3, 'count'], 4);
If we don’t know the index of the entry we want to update. It’s pretty easy to find it using .findIndex():
const indexOfListToUpdate = obj.get('elem').findIndex(listItem => {
return listItem.get('name') === 'third';
});
obj = obj.setIn(['elem', indexOfListingToUpdate, 'count'], 4);
Hope it helps!
var index = list.findIndex(item => item.name === "three")
list = list.setIn([index, "count"], 4)
Explanation
Updating Immutable.js collections always return new versions of those collections leaving the original unchanged. Because of that, we can't use JavaScript's list[2].count = 4 mutation syntax. Instead we need to call methods, much like we might do with Java collection classes.
Let's start with a simpler example: just the counts in a list.
var arr = [];
arr.push(2);
arr.push(1);
arr.push(2);
arr.push(1);
var counts = Immutable.List.of(arr);
Now if we wanted to update the 3rd item, a plain JS array might look like: counts[2] = 4. Since we can't use mutation, and need to call a method, instead we can use: counts.set(2, 4) - that means set the value 4 at the index 2.
Deep updates
The example you gave has nested data though. We can't just use set() on the initial collection.
Immutable.js collections have a family of methods with names ending with "In" which allow you to make deeper changes in a nested set. Most common updating methods have a related "In" method. For example for set there is setIn. Instead of accepting an index or a key as the first argument, these "In" methods accept a "key path". The key path is an array of indexes or keys that illustrates how to get to the value you wish to update.
In your example, you wanted to update the item in the list at index 2, and then the value at the key "count" within that item. So the key path would be [2, "count"]. The second parameter to the setIn method works just like set, it's the new value we want to put there, so:
list = list.setIn([2, "count"], 4)
Finding the right key path
Going one step further, you actually said you wanted to update the item where the name is "three" which is different than just the 3rd item. For example, maybe your list is not sorted, or perhaps there the item named "two" was removed earlier? That means first we need to make sure we actually know the correct key path! For this we can use the findIndex() method (which, by the way, works almost exactly like Array#findIndex).
Once we've found the index in the list which has the item we want to update, we can provide the key path to the value we wish to update:
var index = list.findIndex(item => item.name === "three")
list = list.setIn([index, "count"], 4)
NB: Set vs Update
The original question mentions the update methods rather than the set methods. I'll explain the second argument in that function (called updater), since it's different from set(). While the second argument to set() is the new value we want, the second argument to update() is a function which accepts the previous value and returns the new value we want. Then, updateIn() is the "In" variation of update() which accepts a key path.
Say for example we wanted a variation of your example that didn't just set the count to 4, but instead incremented the existing count, we could provide a function which adds one to the existing value:
var index = list.findIndex(item => item.name === "three")
list = list.updateIn([index, "count"], value => value + 1)
Here is what official docs said… updateIn
You don't need updateIn, which is for nested structures only. You are looking for the update method, which has a much simpler signature and documentation:
Returns a new List with an updated value at index with the return
value of calling updater with the existing value, or notSetValue if
index was not set.
update(index: number, updater: (value: T) => T): List<T>
update(index: number, notSetValue: T, updater: (value: T) => T): List<T>
which, as the Map::update docs suggest, is "equivalent to: list.set(index, updater(list.get(index, notSetValue)))".
where element with name "third"
That's not how lists work. You have to know the index of the element that you want to update, or you have to search for it.
How can I update list where element with name third have its count set to 4?
This should do it:
list = list.update(2, function(v) {
return {id: v.id, name: v.name, count: 4};
});
Use .map()
list = list.map(item =>
item.get("name") === "third" ? item.set("count", 4) : item
);
var arr = [];
arr.push({id: 1, name: "first", count: 2});
arr.push({id: 2, name: "second", count: 1});
arr.push({id: 3, name: "third", count: 2});
arr.push({id: 4, name: "fourth", count: 1});
var list = Immutable.fromJS(arr);
var newList = list.map(function(item) {
if(item.get("name") === "third") {
return item.set("count", 4);
} else {
return item;
}
});
console.log('newList', newList.toJS());
// More succinctly, using ES2015:
var newList2 = list.map(item =>
item.get("name") === "third" ? item.set("count", 4) : item
);
console.log('newList2', newList2.toJS());
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
I really like this approach from the thomastuts website:
const book = fromJS({
title: 'Harry Potter & The Goblet of Fire',
isbn: '0439139600',
series: 'Harry Potter',
author: {
firstName: 'J.K.',
lastName: 'Rowling'
},
genres: [
'Crime',
'Fiction',
'Adventure',
],
storeListings: [
{storeId: 'amazon', price: 7.95},
{storeId: 'barnesnoble', price: 7.95},
{storeId: 'biblio', price: 4.99},
{storeId: 'bookdepository', price: 11.88},
]
});
const indexOfListingToUpdate = book.get('storeListings').findIndex(listing => {
return listing.get('storeId') === 'amazon';
});
const updatedBookState = book.setIn(['storeListings', indexOfListingToUpdate, 'price'], 6.80);
return state.set('book', updatedBookState);
You can use map:
list = list.map((item) => {
return item.get("name") === "third" ? item.set("count", 4) : item;
});
But this will iterate over the entire collection.

Categories