So I'm feeling somewhat confident on updating the array of objects when it's just array of objects using map and the spread function. However, where I'm stuck on is updating an array that is within array of objects.
Here's the example.
I have a seperate state to choose which index
const [option, setOption] = useState(0);
I initialize the state with array of objects
const [parts, setParts] = useState([
{bodyPart: "upperLip", price: 1000, active: [false, false, false]},
{bodyPart: "chin", price: 1000, active: [false, false, false]},
])
Basically, I want to update the array that is nested in array of objects when the description of bodyPart matches up.
const handleOnClick = (bodyPart) => {
parts.map((part) => {
if (part.bodyPart === bodyPart){
return {...part, active[option]=true}
} else {
//do nothing
}
})
}
I know that return {...part, active[option]=true} part is incorrect. What would be the proper way of updating this piece within the state?
I've tried to set it to true like I would for an array, but I'm not too sure anymore.
Related
I'm trying to append array which is react state:
const [ products, setProducts ] = useState([])
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
const copy = JSON.parse(JSON.stringify(products))
copy[category.id] = data
setProducts(copy)
})
})
},[])
service.getCategory() fetches data over HTTP returning array. products is nested array, or at least it's suppose to be. config.category is defined as:
categories: [
{
name: 'product1',
id: 0
},
{
name: 'product2',
id: 1
},
{
name: 'product3',
id: 2
}]
}
Eventually products should be appended 3 times and it should contain 3 arrays containing products from these categories. Instead products array ends up including only data from last HTTP fetch, meaning the final array looks something like this
products = [null, null, [{},{},{},..{}]].
I hope someone knows what's going on? Been tinkering with this for a while now.
The problem is that your fulfillment handlers close over a stale copy of products (the empty array that's part of the initial state). In a useEffect (or useCallback or useMemo, etc.) hook, you can't use any state items that aren't part of the dependency array that you provide to the hook. In your case, you just want to get the data on mount, so an empty dependency array is correct. That means you can't use any state items in the callback.
What you can do instead is use the callback form of the state setter:
const [ products, setProducts ] = useState([]);
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
setProducts(products => { // Use the callback form
const copy = products.slice(); // Shallow copy of array
copy[category.id] = data; // Set this data
return copy; // Return the shallow copy
});
});
});
}, []);
Or more concisely (but harder to debug!) without the explanatory comments:
const [ products, setProducts ] = useState([]);
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
setProducts(products => Object.assign([], products, {[category.id]: data}));
});
});
}, []);
Those both use the same logic as your original code, but update the array correctly. (They also only make a shallow copy of the array. There's no need for a deep copy, we're not modifying any of the objects, just the array itself.)
But, that does a state update each time getCategory completes — so, three times in your example of three categories. If it happens that the request for id 2 completes before the request for id 1 or 0, your array will look like this after the first state update:
[undefined, undefined, {/*data for id = 2*/}]
You'll need to be sure that you handle those undefined entries when rendering your component.
I cannot seem to find an answer on here that is relevant to this scenario.
I have my state in my React component:
this.state = {
clubs: [
{
teamId: null,
teamName: null,
teamCrest: null,
gamesPlayed: []
}
]
}
I receive some data through API request and I update only some of the state, like this:
this.setState((currentState) => {
return {
clubs: currentState.clubs.concat([{
teamId: team.id,
teamName: team.shortName,
teamCrest: team.crestUrl
}]),
}
});
Later on I want to modify the state value of one of the properties values - the gamesPlayed value.
How do I go about doing this?
If I apply the same method as above it just adds extra objects in to the array, I can't seem to target that specific objects property.
I am aiming to maintain the objects in the clubs array, but modify the gamesPlayed property.
Essentially I want to do something like:
clubs: currentState.clubs[ index ].gamesPlayed = 'something';
But this doesn't work and I am not sure why.
Cus you are using concat() function which add new item in array.
You can use findIndex to find the index in the array of the objects and replace it as required:
Solution:
this.setState((currentState) => {
var foundIndex = currentState.clubs.findIndex(x => x.id == team.id);
currentState.clubs[foundIndex] = team;
return clubs: currentState.clubs
});
I would change how your state is structured. As teamId is unique in the array, I would change it to an object.
clubs = {
teamId: {
teamName,
teamCrest,
gamesPlayed
}
}
You can then update your state like this:
addClub(team) {
this.setState(prevState => ({
clubs: {
[team.id]: {
teamName: team.shortName,
teamCrest: teamCrestUrl
},
...prevState.clubs
}
}));
}
updateClub(teamId, gamesPlayed) {
this.setState(prevState => ({
clubs: {
[teamId]: {
...prevState.clubs[teamId],
gamesPlayed: gamesPlayed
},
...prevState.clubs
}
}));
}
This avoids having to find through the array for the team. You can just select it from the object.
You can convert it back into an array as needed, like this:
Object.keys(clubs).map(key => ({
teamId: key,
...teams[key]
}))
The way I approach this is JSON.parse && JSON.stringify to make a deep copy of the part of state I want to change, make the changes with that copy and update the state from there.
The only drawback from using JSON is that you do not copy functions and references, keep that in mind.
For your example, to modify the gamesPlayed property:
let newClubs = JSON.parse(JSON.stringify(this.state.clubs))
newClubs.find(x => x.id === team.id).gamesPlayed.concat([gamesPlayedData])
this.setState({clubs: newClubs})
I am assuming you want to append new gamesPlayedData each time from your API where you are given a team.id along with that data.
I am confused about executing spread operator and using it to update state array like that
todos: [
{
id: "1",
description: "Run",
completed: "true"
},
{
id: "2",
description: "Pick John",
completed: "false"
}]
I have objects inside my array, the examples provided after searching are using spread operator to update arrays with single object, how can I update that object with "id" that equals "key" only. My wrong function is
markTaskCompleted(e) {
var key = e._targetInst.key;
this.setState(
{
todoList: // state.todoList is todos variable
[...this.state.todoList, this.state.todoList.map(task => {
if (task.id == key) task.completed = "true";
}) ]
},
this.getTodos
);
}
The result of this array is the same array of todos (spread operator) with array of undefined items.
I have been googling for some time but couldn't really get it.
Instead of destructuring the array and using map, I typically update a single item's value with a single map that replaces the item I am updating and returns the existing value for all other items. Something like this:
this.setState((prevState) => {
return {
todoList: prevState.todoList.map((task) => {
if (task.id === key) {
return { ...task, completed: true };
} else {
return task;
}
}),
};
});
Also, notice that this example passes a function to this.setState rather than an object. If you are updating the state based on the previous state (in this example using todoList from the previous state) you should use the function method. setState is asynchronous and you could get unexpected results from using this.state to compute the new state.
I have associative array.
It's a key(number) and value(object).
I need to keep state of this array same as it is I just need to update one object property.
Example of array:
5678: {OrderId: 1, Title: "Example 1", Users: [{UserId: 1}, {UserId: 2}, {UserId: 3}]}
5679: {OrderId: 2, Title: "Example 2", Users: [{UserId: 1}, {UserId: 2}, {UserId: 3}]}
I need to update Users array property.
I tried this but it doesn't work:
ordersAssociativeArray: {
...state.ordersAssociativeArray,
[action.key]: {
...state.ordersAssociativeArray[action.key],
Users: action.updatedUsers
}
}
This is data inside reducer.
What I did wrong how to fix this?
Something that might help.
When I inspect values in chrome I check previous value and value after execution of my code above:
Before:
ordersAssociativeArray:Array(22) > 5678: Order {OrderId: ...}
After:
ordersAssociativeArray: > 5678: {OrderId: ...}
Solution (code in my reducer)
let temp = Object.assign([], state.ordersAssociativeArray);
temp[action.key].Users = action.updatedUsers;
return {
...state,
ordersAssociativeArray: temp
}
So this code is working fine.
But I still don't understand why? So I have solution but would like if someone can explain me why this way is working and first not?
If it could help here how I put objects in this associative array initialy:
ordersAssociativeArray[someID] = someObject // this object is created by 'new Order(par1, par2 etc.)'
What you are doing is correct, as demonstrated by this fiddle. There may be problem somewhere else in your code.
Something that I would recommend for you is to separate your reducer into two functions, ordersReducer and orderReducer. This way you will avoid the excessive use of dots, which may be what caused you to doubt the correctness of your code.
For example, something like:
const ordersReducer = (state, action) => {
const order = state[action.key]
return {
...state,
[action.key]: orderReducer(order, action)
}
}
const orderReducer = (state, action) => {
return {
...state,
Users: action.updatedUsers
}
}
I hope you find your bug!
Update
In your solution you use let temp = Object.assign([], state.ordersAssociativeArray);. This is fine, but I thought you should know that it is sometimes preferable to use a {} even when you are indexing by numbers.
Arrays in javascript aren't great for representing normalized data, because if an id is missing the js array will still have an undefined entry at that index. For example,
const orders = []
array[5000] = 1 // now my array has 4999 undefined entries
If you use an object with integer keys, on the other hand, you get nice tightly packed entries.
const orders = {}
orders[5000] = 1 // { 5000: 1 } no undefined entries
Here is an article about normalizing state shape in redux. Notice how they migrate from using an array in the original example, to an object with keys like users1.
The problem can be that you're using array in the state but in the reducer you're putting as object. Try doing:
ordersAssociativeArray: [ //an array and not an object
...state.ordersAssociativeArray,
[action.key]: {
...state.ordersAssociativeArray[action.key],
Users: action.updatedUsers
}
]
It will put ordersAssociative array in your state and not an object.
Let's say we have an immutable object that is created using Facebook's great Immutable.js. I want to compare two lists that were produced using .map or .filter out of single source and make sure they are equal. It seems to me, that when using map/filter you are creating a new object that has nothing to do with a previous object. How can I make triple equality === work? Does it make sense at all?
var list = Immutable.List([ 1, 2, 3 ]);
var list1 = list.map(function(item) { return item; })
var list2 = list.map(function(item) { return item; })
console.log("LIST1 =", list1.toJS()) // [1, 2, 3]
console.log("LIST2 =", list2.toJS()) // [1, 2, 3]
console.log("EQUAL ===?", list1===list2); // false! Why? How?
You can play with it here: http://jsfiddle.net/eo4v1npf/1/
Context
I am building application using React + Redux. My state has one list that contains items, that have attribute selected:
items: [
{id: 1, selected: true},
{id: 2, selected: false},
{id: 3, selected: false},
{id: 4, selected: true}
]
I want to pass only selected ids to another container, so I tried it using simple connect:
function getSelectedIds(items) {
return items
.filter((item) => item.get("selected"))
.map((item) => item.get("id"));
}
export default connect(
(state: any) => ({
ids: getSelectedIds(state.get("items"))
})
)(SomePlainComponent);
Problem is, if I set additional attributes:
{id: 1, selected: true, note: "The force is strong with this one"}
This causes state to change and SomePlainComponent to rerender, although the list of selected Ids is exactly the same. How do I make sure pure renderer works?
Edit with some additional info
For react pure rendering I was using mixin from react-pure-render:
export default function shouldPureComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
As it is not aware of props that could be immutable, they are treated as changed, i.e.
this.props = {
ids: ImmutableList1
}
nextProps = {
ids: ImmutableList2
}
Although both attributes ids are equal by content, they are completely different objects and do not pass ImmutableList1 === ImmutableList2 test and shouldComponentUpdate returns true. #Gavriel correctly pointed that deep equal would help, but that should be the last resort.
Anyway, I'll just apply accepted solution and problem will be solved, thanks guys! ;)
You can never have strict equality of immutable structures since an Immutable.js object, inherently, is unique.
You can use the .is function which takes two immutable objects and compares the values within them. This works because Immutable structures implement equals and hashCode.
var map1 = Immutable.Map({a:1, b:1, c:1});
var map2 = Immutable.Map({a:1, b:1, c:1});
console.log(Immutable.is(map1, map2));
// true
If you want to keep your component pure and working with === then you can also denormalize your Redux state and store the selectedIds as a property in the store. Only update this list when an action occurs that adds/removes a selected item or toggles an item selection, but not when other arbitrary properties of the item are updated.