In my React state, I have the state:
this.state = {
users: [{
id: 1,
name: "john",
age: 27
}, {
id: 2,
name: "ed",
age: 18
}, {
id: 3,
name: "mel",
age: 20
}]
}
I am rendering the name correctly. When you click on the name, it should remove the name, which will need an onClick that takes in a function and that returns a removeUser function.
It is my understanding that you do not want to mutate the state in React, but return a new state. So, in my removeUser function, I did:
removeUser(index) {
// Making a new copy of the array
const users = [...this.state.people].splice(index, 1);
this.setState({ users });
}
I bind my method with this.removeUser = this.removeUser.bind(this).
When I tested out my code, I am removing the users as expected. However, when I run my code against a test that my friend wrote, I got a failed test that said: Expected 1 to be 0
That message tells me that I must be mutating the state somehow, but I am not sure how. Am I returning a new array and updating the state correctly? Can someone explain to me how I should update my state correctly in this case?
Here is the full code:
class Group extends Component {
constructor(props) {
super(props);
this.state = {
users: [
{id: 1, name: "john", age: 27},
{id: 2, name: "ed", age: 18},
{id: 3, name: "mel", age: 20}
]
}
this.removeUser = this.removeUser.bind(this);
}
removeUser(index) {
const users = [...this.state.users].splice(index, 1);
this.setState({ users });
}
render() {
const list = this.state.users.map((user, i) => {
return (
<div onClick={() => this.removeUser(i)} key={i}>{user.name}</div>
);
});
return (
<div>
{list}
</div>
);
}
}
you could also change your onClick and pass it the user.id instead of the index. that would allow you to filter the results instead of needing to splice the array.
onClick={() => this.removeUser(user.id)}
removeUser(id) {
const users = this.state.users.filter((user) => user.id !== id);
this.setState({ users });
}
const users = [...this.state.users].splice(index, 1)
looks like it should be removing an item from a collection, which I suppose it technically is. The problem is that users doesn't contain the list of users you want to keep.
Instead, splice has modified your new array in-place and then returned the removed items:
splice docs
Return value
An array containing the deleted elements. If only one element is removed, an array of one element is returned. If no elements are removed, an empty array is returned.
Instead, if you'd like to use splice, create a new array:
const users = [...this.state.users]
and then splice the new array:
users.splice(index, 1)
You'll run into a similar issue if you need to sort an array.
The issue of modifying data in-place is generally frowned upon for react, in favor of immutable references. This is because React uses a lot of direct comparison of objects as a heuristic approach to speed things up. If your function modifies an existing object instance, React will assume the objects haven't changed.
The act of copying to a new object and then operating on the data comes with some tradeoffs. For removing a single item, it's negligible. For removing many items, you may be better served by an alternative method, such as multiple slice calls:
const users = [
...this.state.users.slice(0, index)
...this.state.users.slice(index + 1)
]
However this too is quite verbose.
Another approach is to use an immutable variant of splice:
// this quick example doesn't handle negative start indices
const splice = (start, deleteCount, ...items) => arr => {
const output = []
let i
for (i = 0; i < start && i < arr.length; i++) {
output.push(arr[i])
}
output.push(...items)
for (i += deleteCount; i < arr.length; i++) {
output.push(arr[i])
}
return output
}
const users = splice(index, 1)(this.state.users)
You should use slice() instead of splice() because splice() mutates the original array, but slice() returns a new array. Slice() is a pure function. Pure is better!!
removeUser(index) {
const users = [
...this.state.users.slice(0, index),
...this.state.users.slice(index+1)
];
this.setState({ users });
}
Here is JS Fiddle
Related
Is there a common known way to chain .map or .filter or .find expressions to accomplish this kind of lookup?
Given and array of objects within an array of objects
customerGroups :
[
{
id: 1,
customers: [{
id: 1, // The same customer may appear in multiple groups
name: 'Jhon'
}],
},
{
id: 2,
customers: [{
id: 2,
name: 'Jhon'
}],
},
{
id: 3,
customers: [{
id: 2,
name: 'Doe'
}],
},
]
In the use case where you have the customer.id and want to find out the customer.name I would like to extract the customers array to use the Array.Find method
const idSearch = 1
const customerName = customers.find(({id})=>id==idSearch).name
So far I been trying with
const customers = customerGroup.find(({ customer }) =>
customer.find(({ id }) =>idSearch === id),
)?.customers
const customerName = customers.find(({id})=>id==idSearch).name
I believe there is a better way to do this but I'm too burnout to figure it out.
I've also tried some shenanigans with the .map to make a new array with all the customers in it but no good results so far.
I could also fetch that array from my Backend but I already have all the customers in memory so that would be an overheat.
There is not one native method that does this, but you could first combine the customer arrays into one with flatMap, and then use find:
const customerGroups = [{id:1,customers:[{id:1,name:'Jhon'}]},{id:2,customers:[{id:2,name:'Jhon'}]},{id:3,customers:[{id:2,name: 'Doe'}]}];
const idSearch = 1;
const allCustomers = customerGroups.flatMap(({customers}) => customers);
const name = allCustomers.find(({id}) => id === idSearch)?.name;
console.log(name);
This approach works because as soon as the inside find loop discovers a result, both the inside and outside loop will terminate, leaving name set as the match which caused the loops to terminate (or as undefined if no match was found).
const d = [{id:1,customers:[{id:1,name:'Jhon'}]},{id:2,customers:[{id:2,name:'Jhon'}]},{id:3,customers:[{id:3,name: 'Doe'}]}]
const idSearch = 1
let name
d.find(j=>j.customers.find(i=>i.id===idSearch && ({name}=i)))
console.log(name)
I am having data file in this array of objects form[{name,state},{name,state},{name,state},{name,state},{name,state}]
I need the state data and number of people belonging to same state, for that i need an array of objects having state and it's count like [{state,count},{state, count},{state,count}]
How can i get this reactjs?
This doesnt have anything to do with ReactJS, which is a library for rendering and managing UI.
In JS you can use a .reduce function to do that. I will produce and array with [{name: string, count: number}]:
const userArray = [{name: Riya, state: US}, {name: Chris, state: DE}]
const stateArray = userArray.reduce((newArray, currentElement) => {
const stateExistsIndex = newArray.findIndex((state) => state.name === currentElement.state)
if (stateExistsIndex !== -1) {
const selectedState = newArray[stateExists]
newArray[stateExists] = { name: selectedState.name, count: selectedState.count++ }
} else {
newArray.push({ name: currentElement.name, count: 1 })
}
return newArray
}, [])
This creates a new array. It loops through the old array and pushes an element to the new array if an state doesnt already exists. If it exists it increases its count by 1. You can of course also do it in a for-of loop which might be more easily understandable.
This is in the context of a node express route. I receive a get request with a query param that is a list of IDs. Now I need to make a call-out for each ID and store the result of the callout in an array or object. Each element of the first array (containing the IDs) need to be mapped to its corresponding result from the call-out. I don't have a way to modify the endpoint that I'm hitting from this route so I have to make single calls for each ID. I've done some research and so far I have a mixture of code and sudo code like this:
const ids = req.query.ids;
const idMembers = Promise.all(ids.map(async id => {
// here I'd like to create a single object or associative array
[ id: await callout(id); ]
}));
When all promises resolved I need the final result of idMembers to be like: (The response will be an object with nested arrays and objects I've just simplified it for this post but I need to grab that from the res.payload)
{
'211405': { name: 'name1', email: 'email1#test.co' },
'441120': { name: 'name2', email: 'email2#test.co' },
'105020': { name: 'name3', email: 'email4#test.co' }
}
Oh and of course I need to handle the callout and the promise failures and that's when my lack of experience with javascript becomes a real issue. I appreciate your help in advance!!
Some extra thought I'm having is that I'd have to map the results of the resolved promises to their id and then in a separate iteration I can then create my final array/object that maps the ids to the actual payloads that contain the object. This is still not answering any of my questions though. I'm just trying to provide as much information as I've gathered and thought of.
Promise.all returns an array of results (one item per each promise).
Having this temporary structure it is possible to build the needed object.
const arrayOfMembers = Promise.all(ids.map(async id => {
// ...
return { id, value: await callout(id) } // short syntax for { id: id, value: ... } (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer)
}));
// arrayOfMembers = [
// { id: 211405, value: { name: 'name1', email: 'email1#test.co' } },
// ...
// ]
In pure JS it can be done with for loop or .forEach() call to iterate:
const res = {};
arrayOfMembers.forEach(el => {
const { id, value } = el;
res[el] = value;
});
or by using a single reduce() call
const res = arrayOfMembers.reduce((accumulator, el) => {
const { id, value } = el;
return { ...accumulator, [id]: value };
}, {});
in both cases res will be:
// res = {
// '211405': { name: 'name1', email: 'email1#test.co' },
// ...
// }
P.S.
There is a handy library called lodash. It has tons of small methods for data manipulation.
For example, _.fromPairs() can build an object from [[key1, value1], [key2, value2]] pairs.
As you mentioned you have lodash, so I think the following should work:
const arrayOfKeyValuePairs = Promise.all(ids.map(async id => {
// ...
return [ id, await callout(id) ] // array here so it matches what fromPairs needs
}));
const res = _.fromPairs(arrayOfKeyValuePairs);
I need a functional approach to solve a very basic problem, the problem with list indexes, let me write an example with React and ramda that shows the need of an index.
const R = require('ramda');
const array = ["foo", "bar", "foobar"];
// Need to put them in a nice html li ?
// this works with a warning that you
// need a unique key to each item.
const renderList = R.map( item => <li>{item}</li> );
// we can solve it like that.
const mapIndexed = R.addIndex(R.map)
const renderListIndexed = mapIndexed((item, id) => <li key={id}>{item}</li>
All of that is cool, but I'm pretty sure the use of an indexed map is not a functional approach, let me know if I'm wrong.
I'm not sure what is Ramda doing since I'm not familiar with React stuff, but if you need an index for elements of your array, you can use basic Array.map function.
const array = ["foo", "bar", "foobar"];
array.map(function(item, index) {
return {
item: item,
id: index
}
});
which will give you an array of objects structured as:
[{ id: 0, item: "foo" }, { id: 1, item: "bar" }, { id: 2, item: "foobar" }]
Hope this helps!
Look at addIndex. There are some very good reasons why Ramda does not include it by default, but this function will mix it in for you.
Try
const renderList = R.addIndex(R.map)( item => <li>{item}</li> );
This question might fall a little on the side of a "best practice" question, but please bear with me.
Here is a portion of my state:
this.state = {
typeElements: {
headers: [
{
name: "h1",
size: 70,
lineHeight: 1.25,
kearning: 0,
marginAfter: 0
}, {
name: "h2",
size: 70,
lineHeight: 1.25,
kearning: 0,
marginAfter: 0
}, {
name: "h3",
size: 70,
lineHeight: 1.25,
kearning: 0,
marginAfter: 0
}...
What I need to do is REPLACE the object at a given index on the headers array.
I don't know how to do that with the setState method as in this.setState(headers[1] = {obj}) - but that's obviously invalid. My current method is creating a new array and clobbering the old one like this:
_updateStyle(props) {
let newState = Object.assign({}, this.state)
newState.typeElements.headers[props.index] = props
this.setState(newState)
};
For my small hacky project I guess it's OK but I feel like this is super heavy handed and would quickly lead to performance issues at any kind of scale.
Updated: since this answer still gets upvotes, be aware that the previous answer below is outdated with modern JavaScript and React. The "update" addon is now legacy and "immutability-helper" can be used instead.
The React docs also mention why immutability is important so avoid mutating state. For immutable updates you can use Object.assign() or spread syntax which needs to be done for every level of nesting, like in this example the nested headers object and its array elements. In this particular example we can use the array index as key so it's possible to also use the spread operator to make a shallow clone of the array and assign a new object as value at given index in the cloned array.
_updateStyle (props) {
const { typeElements } = this.state;
const updatedHeaders = [...typeElements.headers];
updatedHeaders[props.index] = props;
this.setState({
...this.state,
typeElements: {
...typeElements,
headers: updatedHeaders
}
));
}
Another solution which doesn't require the spread syntax and is needed if we are not using the array index to find the object we want to replace, is using array.map to create a new array and returning the new object instead of the old one at given index.
const updatedHeaders = typeElements.headers.map((obj, index) => {
return index === props.index ? props : obj;
});
Similar examples in the Redux docs also explain "immutable update patterns".
React has some immutability helpers for this, which is explained in
the docs: https://facebook.github.io/react/docs/update.html
In your case you could use the $splice command to remove one item and
add the new one at given index, for example:
_updateStyle (props) {
this.setState(update(this.state.typeElements,
{ $splice: [[props.index, 1, props]] }
));
}
Offering a better explanation of how to accomplish this.
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});
}
use immutability-helper
you can find nice examples there
Object.Assign uses shallow copy, not deep copy.
Please be aware that in your example Object.assign({}, this.state) copies only links to the nearest children of the state, i.e. to typeElements, but headers array is not copied.
There is a syntactic sugar ... in ES6 for Object.Assign, Babel has the special addon transform-object-rest-spread.
let newHeaders = { ...this.state.typeElements.headers};
newHeaders[index] = props.Something;
let newState = {...state, typeElements: { newHeaders }};