I have an array of objects stored in state using React
this.state= {
people: [
{
name: 'Tom',
hobbies: ['dancing', 'swimming']
}, {
name: 'Dean',
hobbies: ['playing', 'wondering']
}, {
name: 'Jack',
hobbies: ['shooting', 'hiking']
}, {
name: 'Natalie',
hobbies: ['rock', 'cats']
}
]
};
I want to update the state by removing one specific element from hobbies.
I tried to copy people array from state, then iterate through every person object then through every hobbies array to then check if the element is the one I want to remove, but I didn't manage to remove it, state was not changing.
The thing I tried was to map it and afterwards filter.
What is the easiest and fastest way to do it?
I just started learning React so I want to do it with setTimeout.
At the moment I have only code to choose random hobby from random person.
setTimeout(() => {
const randInst = Math.floor(Math.random() * this.state.people.length);
const hobbyIndex = Math.floor(Math.random() * this.state.people[randInst].hobbies.length);
}, 500);
You should create a new array and then set it as the new value for people in the state. One of the way is to use Array.prototype.map function.
The map() method creates a new array with the results of calling a
provided function on every element in the calling array.
For example, you could do it like this:
const randomPersonIndex = Math.floor(Math.random() * this.state.people.length);
const randomHobbyIndex = Math.floor(Math.random() * this.state.people[randomPersonIndex].hobbies.length);
this.setState({
people: this.state.people.map((person, index) => {
if (randomPersonIndex !== index) {
return person; // not person we are targeting, don't change it
} else {
return {
...person,
hobbies: person.hobbies.filter((v, i) => i !== randomHobbyIndex),
}
}
});
});
I made set up a codesandbox to demonstrate this for you. Check it out here.
Related
If you have an array as part of your state, and that array contains objects, whats an easy way to update the state with a change to one of those objects?
Example, modified from the tutorial on react:
var CommentBox = React.createClass({
getInitialState: function() {
return {data: [
{ id: 1, author: "john", text: "foo" },
{ id: 2, author: "bob", text: "bar" }
]};
},
handleCommentEdit: function(id, text) {
var existingComment = this.state.data.filter({ function(c) { c.id == id; }).first();
var updatedComments = ??; // not sure how to do this
this.setState({data: updatedComments});
}
}
I quite like doing this with Object.assign rather than the immutability helpers.
handleCommentEdit: function(id, text) {
this.setState({
data: this.state.data.map(el => (el.id === id ? Object.assign({}, el, { text }) : el))
});
}
I just think this is much more succinct than splice and doesn't require knowing an index or explicitly handling the not found case.
If you are feeling all ES2018, you can also do this with spread instead of Object.assign
this.setState({
data: this.state.data.map(el => (el.id === id ? {...el, text} : el))
});
While updating state the key part is to treat it as if it is immutable. Any solution would work fine if you can guarantee it.
Here is my solution using immutability-helper:
jsFiddle:
var update = require('immutability-helper');
handleCommentEdit: function(id, text) {
var data = this.state.data;
var commentIndex = data.findIndex(function(c) {
return c.id == id;
});
var updatedComment = update(data[commentIndex], {text: {$set: text}});
var newData = update(data, {
$splice: [[commentIndex, 1, updatedComment]]
});
this.setState({data: newData});
},
Following questions about state arrays may also help:
Correct modification of state arrays in ReactJS
what is the preferred way to mutate a React state?
I'm trying to explain better how to do this AND what's going on.
First, find the index of the element you're replacing in the state array.
Second, update the element at that index
Third, call setState with the new collection
import update from 'immutability-helper';
// this.state = { employees: [{id: 1, name: 'Obama'}, {id: 2, name: 'Trump'}] }
updateEmployee(employee) {
const index = this.state.employees.findIndex((emp) => emp.id === employee.id);
const updatedEmployees = update(this.state.employees, {$splice: [[index, 1, employee]]}); // array.splice(start, deleteCount, item1)
this.setState({employees: updatedEmployees});
}
Edit: there's a much better way to do this w/o a 3rd party library
const index = this.state.employees.findIndex(emp => emp.id === employee.id);
employees = [...this.state.employees]; // important to create a copy, otherwise you'll modify state outside of setState call
employees[index] = employee;
this.setState({employees});
You can do this with multiple way, I am going to show you that I mostly used. When I am working with arrays in react usually I pass a custom attribute with current index value, in the example below I have passed data-index attribute, data- is html 5 convention.
Ex:
//handleChange method.
handleChange(e){
const {name, value} = e,
index = e.target.getAttribute('data-index'), //custom attribute value
updatedObj = Object.assign({}, this.state.arr[i],{[name]: value});
//update state value.
this.setState({
arr: [
...this.state.arr.slice(0, index),
updatedObj,
...this.state.arr.slice(index + 1)
]
})
}
For the following code block:
const items = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
];
const changes = {
name: 'hello'
}
items.forEach((item, i) => {
item = {
...item,
...changes
}
})
console.log(items) // items NOT reassigned with changes
items.forEach((item, i) => {
items[i] = {
...item,
...changes
}
});
console.log(items) // items reassigned with changes
Why does reassigning the values right on the element iteration not change the objects in the array?
item = {
...item,
...changes
}
but changing it by accessing it with the index does change the objects in the array?
items2[i] = {
...item,
...changes
}
And what is the best way to update objects in an array? Is items2[i] ideal?
Say no to param reassign!
This is a sort of a fundamental understanding of higher level languages like JavaScript.
Function parameters are temporary containers of a given value.
Hence any "reassigning" will not change the original value.
For example look at the example below.
let importantObject = {
hello: "world"
}
// We are just reassigning the function parameter
function tryUpdateObjectByParamReassign(parameter) {
parameter = {
...parameter,
updated: "object"
}
}
tryUpdateObjectByParamReassign(importantObject)
console.log("When tryUpdateObjectByParamReassign the object is not updated");
console.log(importantObject);
As you can see when you re-assign a parameter the original value will not be touched. There is even a nice Lint rule since this is a heavily bug prone area.
Mutation will work here, but ....
However if you "mutate" the variable this will work.
let importantObject = {
hello: "world"
}
// When we mutate the returned object since we are mutating the object the updates will be shown
function tryUpdateObjectByObjectMutation(parameter) {
parameter["updated"] = "object"
}
tryUpdateObjectByObjectMutation(importantObject)
console.log("When tryUpdateObjectByObjectMutation the object is updated");
console.log(importantObject);
So coming back to your code snippet. In a foreach loop what happens is a "function call" per each array item where the array item is passed in as a parameter. So similar to above what will work here is as mutation.
const items = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
];
const changes = {
name: 'hello'
}
items.forEach((item, i) => {
// Object assign just copies an object into another object
Object.assign(item, changes);
})
console.log(items)
But, it's better to avoid mutation!
It's better not mutate since this can lead to even more bugs. A better approach would be to use map and get a brand new collection of objects.
const items = [{
id: 1,
name: 'one'
},
{
id: 2,
name: 'two'
},
];
const changes = {
name: 'hello'
}
const updatedItems = items.map((item, i) => {
return {
...item,
...changes
}
})
console.log({
items
})
console.log({
updatedItems
})
As the MDN page for forEach says:
forEach() executes the callbackFn function once for each array
element; unlike map() or reduce() it always returns the value
undefined and is not chainable. The typical use case is to execute
side effects at the end of a chain.
Have a look here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
This means that although you did create new object for item, it was not returned as a value for that index of array. Unlike your second example, the first one is not changing original array, but just creates new objects and returns undefined. This is why your array is not modified.
I'd go with a classic Object.assign for this:
const items = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
];
const changes = {
name: 'hello'
}
items.forEach( (item) => Object.assign(item,changes) )
console.log(items)
Properties in the target object are overwritten by properties in the sources if they have the same key. Later sources' properties overwrite earlier ones.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
The other approach you can take is to use map and create a new array based on the original data and the changes:
const items = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
];
const changes = {
name: 'hello'
}
const newItems = items.map((item) => {
...item,
...changes
})
console.log(newItems);
But if you need to modify the original array, it's either accessing the elements by index, or Object.assign. Attempting to assign the value directly using the = operator doesn't work because the item argument is passed to the callback by value not by reference - you're not updating the object the array is pointing at.
I created two functions that change the state:
class App extends Component {
state = {
counters: [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 0 },
{ id: 4, value: 4 },
],
};
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
...
above code works and change the state, I then created slightly shorter form of above function handleIncrement but it didn't work
handleIncrement = (counter) => {
this.setState({
counters: this.state.counters[this.state.counters.indexOf(counter)]
.value++,
});
in above approach I used setState and didn't change the state directly. So what is the problem with it?
Your "slightly shorter form" does something completely different than the original code. this.state.counters is an array of objects. In your first example, you correctly update that array by changing the value in one of the objects in the array. In your second example, you replace the array with the result of this.state.counters[this.state.counters.indexOf(counter)].value++ which is a number not an array.
You probably meant to do something like this instead:
handleIncrement = (counter) => {
this.state.counters[this.state.counters.indexOf(counter)].value++;
this.setState({
counters: this.state.counters,
});
This increments the value inside the array and then calls setState() by passing in the array for the key counters. However, mutating state directly like this is considered poor practice in React because it is easy to forget to call setState() to initiate rendering our components. Instead, we create a copy and update the copy and pass that to setState().
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 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 ...