Hooks and immutable state - reactjs

I am apologising in advance if this questions has already been answered before (and also for the long post, but I've tried to be as specific as I could be). But, the answers I found does not completely satisfy me.
I want to use the new amazing React Hooks for my project. And for what I've been doing so far, it has been straight forward.
Now I have ran into a more complex example, and I feel unsure on how I best should tackle this one.
Let's say, I have more of a complex object (at least it's not flat) in my state.
{
persons: [
{
id: 1,
name: 'Joe Doe',
age: 35,
country: 'Spain',
interests: [
{ id: 1, en: 'Football', es: 'FĂștbol' },
{ id: 2, en: 'Travelling', es: 'Viajar' }
]
},
{
id: 2,
name: 'Foo Bar',
age: 28,
country: 'Spain',
interests: [
{ id: 3, en: 'Computers', es: 'Computadoras' },
{ id: 4, en: 'Cooking', es: 'Cocinar' }
]
}
],
amount: 2,
foo: 'bar'
}
What is the best way to:
Add an item (an object) to my colleagues array
Add an item to a specific "interests" array?
Manipulate the value of a property within an object in the array?
Change a value outside the persons array, for example foo?
using the useState hook?
The following code examples will try to illustrate each question. They're not tested...
Let us consider I have a container that begins with this.
It also includes the functions that are split up in the rest of the post.
const [colleagues, setColleagues] = useState({});
// using the useEffect as "componentDidMount
useEffect(() => {
// receiving data from ajax request
// and set the state as the example object provided earlier
setColleagues(response.data);
}, []);
1) So for the first question. Is this valid?
Or do I need to to make sure that each and every object in my persons array is destructured?
const onAddNewColleague = () => {
// new colleague, this data would be dynamic
// but for the sake of this example I'll just hard code it
const newColleague = {
name: 'Foo Baz Bar',
age: 30,
country: 'Spain',
interests: [
{ en: 'Cats', es: 'Gatos' }
]
};
// creates a copy of the state
// targets the "persons" prop and adds a new array
// where the new colleague is appended
const newState = {
...colleagues,
persons: colleagues.concat(newColleague)
};
// updates the state
setColleagues(newState);
};
2) This feels wrong as I end up updating the entire persons array instead of just the interest array for a specific person.
const onAddNewInterest = (personId) => {
// a new interest
const newInterest = {
en: 'Languages', es: 'Idiomas'
};
// find the person according to personId
// and update the interests
const updatedPersons = colleagues.persons.map(person => {
if(person.id === personId) {
return {
...person,
interests: person.interests.concat(newInterest);
};
}
return person;
});
// create a copy of the state
const newState = {
...colleagues,
persons: [...updatedPersons]
};
setColleagues(newState);
};
3) As the second example, this one feels wrong too as I am updated the entire persons array when in fact I might just want to change the age of one specific person
const onChangeValue = (personId, key, value) => {
// find the person
const updatedPersons = colleagues.persons.map(person => {
if(person.id === personId) {
// key, could be age?
return {
...person,
[key]: value
};
}
return person;
});
// create a copy of the state
const newState = {
...colleagues,
persons: [...updatedPersons]
};
setColleagues(newState);
};
4) Is this valid, or do I need to destruct every part of my colleagues object separately?
const onChangeOtherValue = (key, value) => {
// for example changing the value foo
const newState = {
...colleagues,
[key]: value
};
setColleagues(newState);
};
I do have a feeling that only the concept of the first function is valid, while the rest of them are not.
Can this be done easily, or should I just use an immutable-helper?
Thanks in advance!
Updated examples to get syntax, right. Thanks Valerii.
To clarify
What I'm really after here is best practise to handle use cases like this one. I want to make sure my state is updated in the most correct and efficient way. So feel free to rip my examples a part or write new ones - I'm all ears. It is not necessary to simply modify mine to fit this post (unless they actually turn out to be good).

1) OK
2)
const updatedPersons = colleagues.persons.map(person => {
if(person.id === personId) {
return {
...person,
interests: person.interests.concat({ en: 'Test', es: 'Test' })
};
}
return person;
});
const newState = {
...colleagues,
persons: updatedPersons
};
3)
const updatedPersons = colleagues.persons.map(person => {
if(person.id === personId) {
return {
...person,
[key]: value
};
}
return person;
});
// create a copy of the state
const newState = {
...colleagues,
persons: updatedPersons
};
4) OK

for the first one, i would do this way,
const newState = {
...colleagues,
persons: [...persons, newColleague]
};

Related

Is it valid, to update the state by mapping to new Objects

Let's take this Update State example:
const initialState = [
{id: 1, country: 'Austria'},
{id: 2, country: 'Belgium'},
{id: 3, country: 'Canada'},
];
const [data, setData] = useState(initialState);
const updateState = () => {
setData(prevState => {
const newState = prevState.map(obj => {
if (obj.id === 2) {
return {...obj, country: 'Denmark'};
}
return obj;
});
return newState;
});
};
1. Is it also valid to update the state like this? (First example)
const updateState = () => {
const newState = data.map(obj => {
if (obj.id === 2) {
return {...obj, country: 'Denmark'};
}
return obj;
});
setData(newState);
};
2. Is it also valid to update the state like this? (Second example)
const updateState = () => {
setData(prevState => {
const newState = prevState.map(obj => {
if (obj.id === 2) {
let newObj = obj;
newObj.country = 'Denmark'
return newObj;
}
return obj;
});
return newState;
});
};
3. Do this specific versions also have performance impacts? Which one is the best?
The first and the second example are perfectly valid. I would, however, suggest you to use the first one and I will explain why:
With the first example you are using a callback as an argument of the function. And this form means that you are actually getting the last data state value (this is important because the state updates happen asynchronously). Whenever you need to update the state based on the previous value even React suggests to use the callback form to avoid side effects.
More infos here: https://reactjs.org/docs/hooks-reference.html#functional-updates
The third example is not valid because you are mutating directly the state. Something that in react is not allowed.
More infos here: https://dev.to/il3ven/common-error-accidentally-mutating-state-in-react-4ndg

how to update react state to add an element in array, which is nested array?

const [persons, setPersons] = useState([
{ name: "", age: "", phoneNumbers: [''] },
]);
if we need to add a new person, we can use following syntax...
const addPersonFields = () => {
let person = {
name: "",
age: "",
phoneNumbers: [""],
};
setPersons([...persons, person]);
};
But how to add new phone number for the person at index "i" and make that reflect in react state?
Assuming you know the index of the person you want to update, you could do something like:
const addPersonPhoneNumber = (index, newNumber) => {
const newState = [persons];
newState[index].phoneNumbers.push(newNumber);
setPersons([...newState]);
}
Another way, we can add the phone number of a person at index i by using setPersons directly
const addPhoneNumber = (index, phoneNumber) => {
setPersons((prevPersons) => {
prevPersons[index].phoneNumbers.push(phoneNumber);
return [...prevPersons];
});
}
You would have to find the person at first. The solution below suggests you already have the person object you want to add a phone number to.
const addPhoneNumber = (newNumber) => {
let index = persons.map((e) => e).indexOf(person);
persons[index].phoneNumbers.push(newNumber);
setPersons([...persons]);
};

Trying to delete a property in an array that is also within in array in React

I'm trying to add functionality to my application for deleting values in the list array of the following state
shoeList : [
{name: 'Nike',
list : [
{type: 'boots', revenue: '1000000', gender: 'mens', price: '49.99', id: 3},
{type: 'shoes', revenue: '13280100', gender: 'womens', price: '99.99', id: 2}
]
}
],
I understand that ids have to be equal in order for a deletion to occur. Right now i'm trying to access the appropriate row to be deleted(type, revenue, gender, and price) but am not sure how to access properties in an array thats within an array.
Here is the code i have so far
deleteShoe = (id) =>
{
let shoeList = this.state.shoeList.filter(shoe =>{
return (
shoe.list.filter(shoeid =>{
return shoeid.id !== id
}
)
)
});
this.setState({
shoeList: shoeList
})
}
Obviously this code doesn't and can't understand why. The only reason I can think of is that filter can't be nested but if so how would I go about implementing this. Any help would be great
You're close. You're just missing reassignment of shoe.list with updated list of shoes after filtering.
deleteShoe = (id) => {
const { shoeList } = this.state;
const newShoeList = shoeList.map(shoe => {
shoe.list = shoe.list.filter(shoeid => shoeid.id !== id)
return shoe;
});
this.setState({
shoeList: newShoeList
})
}
This should remove appropriate shoe id and reassign your shoe.list with newly filtered shoes list.

Adding to an array within an array state in ReactJS

I'm trying to add functionality for adding to a state, more specifically "list", in the following state:
shoeList : [
{name: 'Nike',
list : [
{type: 'boots', revenue: '1000000', gender: 'mens', price: '49.99', id: 3},
{type: 'shoes', revenue: '13280100', gender: 'womens', price: '99.99', id: 2}
]
}
],
Right now I have a component that displays a form for the user to enter new values for type revenue gender and price.
Here is the code for the component(not including the forms and text input html):
state = {
}
//when changes occur in text input fields
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault();
this.props.addShoe(this.state);
And in the root component i have the addShoe function:
addShoe = (shoe) => {
shoe.list.id = Math.random();
//returns a new array so no alteration of original array
let shoeList = [...this.state.shoeList, shoe];
this.setState({
shoeList: shoeList
})
}
Trying this code gives me an error saying shoe.list.id is undefined? Also I think I'm missing something to add in the component file specifically in the state. Also is there any way to directly access list like this.state.shoeList.list? I'm not sure if i have to add list to shoeList. Any help would be great thanks
In your example, if the intention is to add an item to specifically the Nike list:
addShoe = (shoe) => {
this.setState({
// return an altered copy of the array via map
shoeList: this.state.shoeList.map(brandItem => {
if (brandItem.name === 'Nike') {
return {
// use spread syntax for other object properties to persist
...brandItem,
list: [
// use spread syntax to keep the original items in the list
...brandItem.list,
{
// assuming shoe is an object without an id property
...shoe,
id: Math.random()
}
]
}
} else {
return brandItem;
}
})
}

Redux state is being updated without dispatching any action

I should start off by saying this is not a duplicate of this question, which happened to have the same title.
I'm simply getting a customers object of arrays from props inside a componentDidMount method like this;
componentDidMount() {
const { customers } = this.props
const expiringCustomers = getExpiringCustomers(customers)
console.log('Expiring customers ', expiringCustomers)
}
Inside another file, I have that getExpiringCustomers function which takes the customers passed and is suppose to return a newly modified list of customers like this;
function numbersOnly(value) {
if(_.isString(value)) {
value = Number(value.replace(/[^\d]/g, ''))
}
return value
}
function normalizeNumber(collection, field) {
return collection.map(obj => {
obj[field] = numbersOnly(obj[field])
return obj
})
}
export function getExpiringCustomers(customers) {
const expiringCustomers = customers.filter(customer => {
const daysLeft = Number(new Date(customer.endDate)) - _.now()
if(daysLeft <= (dateInMonth * 3)) {
return customer
}
})
return normalizeNumber(expiringCustomers, 'rent')
}
I'm connecting my react component with redux state like this;
const mapStateToProps = state => ({
customers: state.customers.filter(customer => customer && !customer.deleted)
})
export default connect(mapStateToProps)(Accounting)
Problem
After the functions run and log results, customers' state is changed in redux store.
This is very confusing as customers_edit action has to pass through some procedures but none of them are called/logged.
Snapshot of the affected object:
Ps. The data is just boilerplate.
//- Focus on rent property
const customers = [
...,
{
id: 'o91wukyfsq36qidkld02a0voo93rna5w',
cardId: 'GD-1101010111',
id_type: 'Driving License',
firstName: 'Maalim',
lastName: 'Guruguja',
names: 'Maalim Guruguja',
property: '5iaprurefg3v3uhad688mypo9kqf6xk3',
rent: '250,000',
email: 'tonimarikapi#yahoo.com',
phone: '239-288-3838-38',
noticePeriod: '3',
status: '2 months remain',
startDate: '2018-07-09',
endDate: '2018-08-17',
createdAt: 1530623480772,
updatedAt: 1531213159147
},
...
]
//- After the functions run, log and edit customers array
const customers = [
...,
{
id: 'o91wukyfsq36qidkld02a0voo93rna5w',
cardId: 'GD-1101010111',
id_type: 'Driving License',
firstName: 'Maalim',
lastName: 'Guruguja',
names: 'Maalim Guruguja',
property: '5iaprurefg3v3uhad688mypo9kqf6xk3',
rent: 250000,
email: 'tonimarikapi#yahoo.com',
phone: '239-288-3838-38',
noticePeriod: '3',
status: '2 months remain',
startDate: '2018-07-09',
endDate: '2018-08-17',
createdAt: 1530623480772,
updatedAt: 1531213159147
},
...
]
From the linked question (possible duplicate one) the guy who answered stated that it's some mutation issue that may cause this. I'm not sure if that applies on props that are suppose to be read-only.
How can I stop these functions from updating my redux store, please help.
You mutate the objects in normalizeNumber, since all the array methods you use don't clone the array's objects.
Change normalizeNumber callback to return a new object with the updated field:
function normalizeNumber(collection, field) {
return collection.map(obj => ({
...obj,
[field]: numbersOnly(obj[field])
}))
}
It looks like you're modifying the customers array unintentionally.
Try:
componentDidMount() {
const { customers } = { ...this.props };
const expiringCustomers = getExpiringCustomers(customers)
console.log('Expiring customers ', expiringCustomers)
}

Resources